Devolutions / Devolutions/IronRDP

connector: server Auto-Detect Requests sent after the ConnectTimeAutoDetection phase kill the connection (license exchange, then capabilities exchange)

Open
#1,629 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
3.2k
Forks
275
Avg merge
1d 11h
Merged PRs (30d)
189

Description

`ClientConnector` answers Auto-Detect Requests only inside its own `ConnectTimeAutoDetection` phase, which ends at the first PDU off the MCS message channel. A Windows RDS host we connect to keeps sending `SEC_AUTODETECT_REQ` probes *after* that phase has closed. Every later phase hands whatever arrives straight to its own decoder, so the connection dies before the session opens.

`mstsc` connects to the same host without complaint, and other RDP hosts work — they simply never start a licensing exchange.

**Environment**

- `ironrdp-connector` 0.10.0, `ironrdp-pdu` 0.9.0, `ironrdp-session` 0.11.0, `ironrdp-core` 0.2.1 (latest published on crates.io)
- Server: Windows host with RDS licensing (CAL) — licensing exchange runs, `mstsc` fine
- Client: custom sans-I/O driver on the connector's `Sequence` API

**Symptom**

```
ConnectFailed("[decode during SERVER_NEW_LICENSE/LicenseExchangeState::UpgradeLicense
@ .../core/src/ops/function.rs:250] decode error")
```

The licensing exchange runs correctly right up to the last step — `ClientNewLicenseRequest` sent, `ServerPlatformChallenge` received, `ClientPlatformChallengeResponse` sent — and then the next PDU fails to decode.

The `SERVER_NEW_LICENSE` in that message is only the state's static `with_context` label; it says nothing about which PDU actually arrived. It sent our first round of diagnosis down the wrong path entirely (we assumed a truncated licensing PDU).

**What actually arrives**

Upstream prints no bytes with that error, so we had to patch the crate locally just to log them:

```
user_data_len = 10
user_data = [0, 16, 0, 0, 6, 0, 0, 0, 20, 0]
error = invalid `securityHeaderFlags`
```

| Field | Bytes | Value |
| --- | --- | --- |
| `BasicSecurityHeader.flags` | `00 10` | `0x1000` — **SEC_AUTODETECT_REQ**, not `SEC_LICENSE_PKT` (`0x0080`) |
| `headerLength` | `06` | 6 |
| `headerTypeId` | `00` | `TYPE_ID_AUTODETECT_REQUEST` |
| `sequenceNumber` | `00 00` | 0 |
| `requestType` | `14 00` | `0x0014` — **RDP_RTT_REQUEST** |

So it is not a licensing PDU at all. `license_exchange.rs` feeds whatever arrives into `LicensePdu::decode` without checking the security header, and an unrelated advisory PDU takes down the whole connection.

Fixing that alone only moves the failure: with licensing completing, the same probe then reaches `decode_share_control` in `connection_activation.rs`'s `CapabilitiesExchange`.

**Suggested shape of a fix**

What worked for us, in a local fork of 0.10.0 (~97 changed lines across `license_exchange.rs`, `connection_activation.rs` and one `pub(crate)` in `connection.rs`):

1. `license_exchange.rs` — each of the three receiving states (`NewLicenseRequest`, `PlatformChallenge`, `UpgradeLicense`) checks `BasicSecurityHeader.flags` for `SEC_LICENSE_PKT` before decoding. A payload without it is logged at DEBUG and skipped, and the state machine **stays in the same state** to await the real licensing PDU.
2. `connection_activation.rs` — in `CapabilitiesExchange`, decode the `AutoDetectReqPdu` and answer it through `connection::respond_to_connect_time_autodetect`, then stay in the state still waiting for Server Demand Active. This is the same skip-and-stay shape the code already applies a few lines below for `ServerDeactivateAll`.

Staying put is the load-bearing part. An earlier attempt treated the undecodable packet as the end of licensing and advanced to `LicenseExchanged`; the exchange was then left half-finished and the connection failed one step later instead. Skipping and waiting matches what MS-RDPBCGR implies: auto-detect is advisory, and a client that never answers an RTT probe is fine — the server proceeds regardless.

Verified against the live host, in order: probe skipped mid-licensing, `ServerUpgradeLicense` (2196 bytes) received, `License verified with success`, probe answered during Capabilities Exchange, eight Connection Finalization steps, `Connected with success`, frames rendering.

We deliberately did **not** touch `ironrdp-pdu` — the parser is fine; the issue is purely which PDUs get handed to it.

**Relationship to existing work**

- #1457 / #1458 (`tolerate unknown security header flags in BasicSecurityHeader`) is a different failure on the same server class. That one is a genuine licensing PDU with flags the header rejected; here the PDU is not a licensing PDU at all, so tolerating the flags would just hand auto-detect bytes to the licensing parser. Also worth noting that #1458 is merged but not yet published — `ironrdp-pdu` on crates.io is still 0.9.0 (2026-07-10).
- #1559 (`count the auto-detect header, and only answer connect-time`) narrows behaviour *inside* the auto-detect phase, so that continuous-detection Starts do not open a connect-time window. It does not address PDUs arriving after the phase has closed, which is what this report is about. The two look adjacent but touch different code paths.

Happy to open a PR with the patch above if the maintainers agree with the shape — in particular whether answering the probe in `CapabilitiesExchange` (rather than skipping it as licensing does) is the behaviour you want.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with license_exchange.rs and connection_activation.rs, then inspect connection.rs and the existing ServerDeactivateAll handling. Trace how non-licensing Auto-Detect PDUs are decoded in each receiving state and how respond_to_connect_time_autodetect is used. Done means the licensing and capabilities exchanges tolerate the probes and the reported host reaches a successful connection.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.