Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,172 changes: 600 additions & 572 deletions Cargo.lock

Large diffs are not rendered by default.

368 changes: 368 additions & 0 deletions crates/ironrdp-acceptor/CHANGELOG.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions crates/ironrdp-acceptor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironrdp-acceptor"
version = "0.10.0"
version = "0.11.0"
readme = "README.md"
description = "State machines to drive an RDP connection acceptance sequence"
edition.workspace = true
Expand All @@ -17,11 +17,11 @@ doctest = false
test = false

[dependencies]
ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public
ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public
ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public
ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public
ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public
ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public
ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public
ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public
rand = "0.9"
tracing = { version = "0.1", features = ["log"] }

Expand Down
18 changes: 9 additions & 9 deletions crates/ironrdp-activex/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ doctest = false
[target.'cfg(windows)'.dependencies]
anyhow = "1"
base64 = "0.22"
ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["clipboard", "dvc-com-plugin", "gateway", "location", "rdpdr", "rustls", "smartcard", "sound", "webauthn"] }
ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7" }
ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.2.0" }
ironrdp-client = { path = "../ironrdp-client", version = "0.2", features = ["clipboard", "dvc-com-plugin", "gateway", "location", "rdpdr", "rustls", "smartcard", "sound", "webauthn"] }
ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" }
ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.3.0" }
ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.7" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" }
ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" }
ironrdp-core = { path = "../ironrdp-core", version = "0.2" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" }
ironrdp-core = { path = "../ironrdp-core", version = "0.3" }
ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" }
ironrdp-input = { path = "../ironrdp-input", version = "0.7" }
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" }
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" }
ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" }
ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1.0" }
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" }
ironrdp-rail = { path = "../ironrdp-rail", version = "0.1" }
ironrdp-rdpdr-native = { path = "../ironrdp-rdpdr-native", version = "0.7" }
ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" }
ironrdp-session = { path = "../ironrdp-session", version = "0.11" }
ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" }
ironrdp-session = { path = "../ironrdp-session", version = "0.12" }
ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" }
ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" }
png = "0.18"
sha2 = "0.10"
Expand Down
479 changes: 479 additions & 0 deletions crates/ironrdp-agent/CHANGELOG.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions crates/ironrdp-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironrdp-agent"
version = "0.1.0"
version = "0.2.0"
readme = "README.md"
description = "CLI-driven, daemon-backed agentic RDP client suitable for LLM consumption"
edition.workspace = true
Expand All @@ -22,7 +22,7 @@ test = false
[dependencies]
# Configuration model and codecs
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1" }
ironrdp-input = { path = "../ironrdp-input", version = "0.7" }
ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" }
Expand All @@ -38,7 +38,7 @@ clap = { version = "4.6", features = ["derive", "cargo", "env"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.1", features = ["rustls"] }
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", features = ["rustls"] }

[target.'cfg(windows)'.dependencies]
# Windows Sandbox control plane: gRPC/HTTP2 over a per-user named pipe.
Expand Down
40 changes: 40 additions & 0 deletions crates/ironrdp-ainput/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.8.0...ironrdp-ainput-v0.9.0)] - 2026-09-30

### <!-- 1 -->Features

- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec))

## Summary

The workspace sweep that follows #1266. Decode and encode error
construction sites now pass the cursor, so the reported position is the
byte the decoder or encoder actually stopped at.

Stacked on #1266 and merges after it.

## What "no position" means here

#1266 makes `offset` an `Option<usize>` where `None` means the error has
no position in the input stream at all, rather than a position that
happened to be unavailable. This PR is the other half of that: it walks
the workspace and gives a real position to every site that has one, so
the sites left reporting `None` are the ones that genuinely never had
one.

Those are constructors validating their arguments, integer conversions,
cache lookups that missed, accessors on already-decoded structures, and
the declared-size checks described below. They report nothing rather
than byte zero, and that is now their permanent answer rather than a gap
awaiting another sweep.

There are no `at: 0` sites left anywhere in the workspace.

## The rule

The position is attached where the cursor identifies the bytes being
complained about. It is omitted where the complaint is about a size the
peer declared, computed from data already consumed, because there the
cursor points at a byte that is not the problem.



## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.7.0...ironrdp-ainput-v0.8.0)] - 2026-07-10

### <!-- 7 -->Build
Expand Down
6 changes: 3 additions & 3 deletions crates/ironrdp-ainput/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironrdp-ainput"
version = "0.8.0"
version = "0.9.0"
readme = "README.md"
description = "AInput dynamic channel implementation"
edition.workspace = true
Expand All @@ -17,8 +17,8 @@ doctest = false
test = false

[dependencies]
ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public
ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public
bitflags = "2.11"
num-derive.workspace = true # TODO: remove
num-traits.workspace = true # TODO: remove
Expand Down
216 changes: 216 additions & 0 deletions crates/ironrdp-async/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,222 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.10.0...ironrdp-async-v0.11.0)] - 2026-09-30

### <!-- 0 -->Security

- [**breaking**] Implement multitransport bootstrapping handshake ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)) ([e45fbfe0f5](https://github.com/Devolutions/IronRDP/commit/e45fbfe0f597011706e77fc174ca14e5e9d435b9))

## Summary

Makes the `MultitransportBootstrapping` state functional instead of a
no-op
pass-through. After licensing the server may send 0, 1, or 2 Initiate
Multitransport Request PDUs before capabilities exchange. Each one is
surfaced
to the application, which establishes UDP transport (RDPEUDP2 + TLS +
RDPEMT)
or declines, and the connector reports the outcome back to the server.

## API

Mirrors the existing `should_perform_X()` pause-point pattern used by
TLS
upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than
`mark_X_as_done()` because completion carries result data:

- `should_perform_multitransport()`: true while a request awaits an
outcome
- `multitransport_request()`: the request awaiting an outcome, or `None`
- `complete_multitransport(result, output)`: report the outcome, resume
- `skip_multitransport(output)`: decline, resume

`complete_multitransport` accepts a `MultitransportResult` (a `Success`
/
`Failure(hresult)` enum) rather than a caller-built response PDU. The
connector
builds the response internally from the stored request ID.

Requests are surfaced one at a time rather than as a batch. There is no
end
marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act
on a
request as soon as it decodes one, so waiting to learn how many are
coming is
not an option the protocol offers. `should_perform_multitransport()` can
therefore come round twice; the caller answers reliable and lossy
separately.

## Approach

**Routing.** Requests arrive on the negotiated MCS message channel
(2.2.15.1) and the Demand Active on the I/O channel, so the channel
decides
which is which. The message channel also carries NetworkAutoDetect since
#1348,
so a decode still confirms what arrived there, but the I/O channel is
never
speculatively decoded as multitransport. A PDU on neither channel is an
error.

For the decode to be a sound confirmation the request decoder must
reject a
Demand Active, so this PR also tightens `MultitransportRequestPdu` to
require
the exact `SEC_TRANSPORT_REQ` security-header flag.

**Yielding.** Each request is surfaced the moment it decodes. Responding
returns the connector to `MultitransportBootstrapping` to read whatever
comes
next, which may be a second request or the Demand Active. Nothing is
buffered
and nothing is replayed: when the request is surfaced the Demand Active
has not
arrived yet.

**Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync
signalling path (2.2.15.2), permitted only when both peers advertised
`SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The
server's
block is retained from the GCC exchange and checked against the client's
configured flags. One rule covers both paths:

- Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including
on
`skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and
blocking drivers skip automatically, so without this every default
client
leaves a compliant server waiting.
- Not negotiated: never respond. The outcome is reported in band on the
new
transport, and putting anything on the main channel would be the
violation.

The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If
Soft-Sync was negotiated but no message channel exists the connector
errors
rather than falling back to the I/O channel, and that check runs before
the
pending state is taken, so the caller is left with a connector it can
still
inspect or decline from.

## Wire behaviour

On the wire TCP and UDP negotiation happen in parallel: the UDP
transport is
established alongside the ongoing TCP handshake, and its completion
signals the
dynamic-channel layer that subsequent channels may migrate to UDP. The
connector's API yield point here is a Rust affordance, not a
spec-mandated TCP
pause. Thanks to @hardening for the correction.

## Tests

Connector state-machine tests in `ironrdp-testsuite-core` drive the
public API
with the shared `SERVER_DEMAND_ACTIVE` fixture:

- a request is surfaced on arrival, without waiting for a following PDU
(regression test for the stall);
- responding returns to bootstrapping so a second request is read
normally;
- a third request is rejected per the 2.2.15.1 cap;
- a Demand Active on the I/O channel ends bootstrapping;
- the response targets the message channel, decoded back off the wire;
- a `Failure` result is carried through;
- `skip` sends `E_ABORT` under Soft-Sync, and nothing without it;
- `complete` emits nothing without Soft-Sync but still resumes;
- a failed response leaves the connector in `MultitransportPending`,
still able
to report or decline, rather than `Consumed`;
- `complete` / `skip` outside `MultitransportPending` error;
- a Demand Active's user data does not decode as a
`MultitransportRequestPdu`
(regression test for the decoder tightening above).

### <!-- 1 -->Features

- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6))

Adds Hyper-V VMConnect's direct ordering: PCB β†’ TLS β†’ CredSSP β†’ X.224.

Enhanced Session is the default (`GUID;EnhancedMode=1`), with
`--vmconnect-basic` for the synthetic console. Kept this separate in
`ironrdp-vmconnect`; no SPN changes.

Tested against the nested Hyper-V lab:
- Enhanced: `HYBRID_EX`, rendered 1280Γ—720
- Basic: `HYBRID`, rendered 1280Γ—720
- `cargo xtask check fmt/lints/tests -v`

---------

- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55))

Hyper-V over RDCleanPath needs PCB β†’ TLS on the proxy, then CredSSP β†’
X.224 on the client. Ordinary RDCleanPath stays X.224-first.

Still VERSION_1 with the same DER fields. An explicit VMConnect request
carries a Unicode PCB payload in `preconnection_blob` with no X.224; the
proxy encodes the binary PCB. Generic PCB requests keep their existing
X.224-first behavior.

Gateway reference implementation:
[Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372)

Checked locally: Rust builds, formatting, Svelte typecheck, and .NET
build. Real nested Hyper-V E2E through Gateway: Native rendered 18
frames, Avalonia connected and rendered its first frame, and Web
rendered a non-empty 1280Γ—720 canvas.

---------

- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87))

## Summary

- Connect-time bandwidth measurement needs to know when bytes arrived,
and nothing in the sans-I/O layer could tell it. #1465, now merged,
answers the server's Bandwidth Measure Stop with a nominal interval for
exactly that reason: the connector has no way to observe the real one.
- Introduce `MonotonicInstant`, a millisecond counter with an arbitrary
epoch, and make `Option<MonotonicInstant>` a required parameter of
`Sequence::step`. The I/O drivers already know when a read completed, so
`Framed` records the arrival time of each read and hands it to the state
machine. A driver with no clock passes `None`.
- With arrival times available, measure for real: a Bandwidth Measure
Start opens a window, Payload messages accumulate their byte counts, and
Stop reports the elapsed time between its own arrival and the Start's.

#1465 has merged, so this applies directly to master and carries no
merge-order dependency. That PR was the FreeRDP unblock on its own; this
is the design change behind it, split out at @CBenoit's suggestion in
review.

## Why the clock lives in the driver

Two reasons, both of which rule out having the sequence read a clock
itself.

- Delegate multitransport setup ([#1858](https://github.com/Devolutions/IronRDP/issues/1858)) ([7036fb8c7e](https://github.com/Devolutions/IronRDP/commit/7036fb8c7ef32f71b456745902e14d34008c7add))

Let applications establish negotiated multitransport channels while the
async connector retains protocol sequencing and response ownership.

Expose Soft-Sync state to the setup callback, centralize response
construction, and report callback failures with E_ABORT when possible.
The existing finalizer still declines multitransport and uses TCP.

### <!-- 4 -->Bug Fixes

- Reject a zero-length unmatched PDU in read_by_hint ([#1556](https://github.com/Devolutions/IronRDP/issues/1556)) ([2d2c37fc21](https://github.com/Devolutions/IronRDP/commit/2d2c37fc21bd7f089fbbca41831abba14fa2b72b))

## Problem



## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.9.0...ironrdp-async-v0.10.0)] - 2026-07-10

### <!-- 7 -->Build
Expand Down
8 changes: 4 additions & 4 deletions crates/ironrdp-async/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironrdp-async"
version = "0.10.0"
version = "0.11.0"
readme = "README.md"
description = "Provides `Future`s wrapping the IronRDP state machines conveniently"
edition.workspace = true
Expand All @@ -17,9 +17,9 @@ doctest = false
test = false

[dependencies]
ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public
ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public
ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public
ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public
tracing = { version = "0.1", features = ["log"] }
bytes = "1" # public
web-time = "1.1"
Expand Down
Loading