feat(connector)!: pass frame arrival time into Sequence::step - #1530
feat(connector)!: pass frame arrival time into Sequence::step#1530Greg Lamberson (glamberson) wants to merge 4 commits into
Conversation
595f9fb to
88d6870
Compare
…RDP servers (#1465) ## Summary - The connector answers only the RTT auto-detect request at connect time and returns nothing for the Bandwidth Measure Stop, on the assumption that skipping it does not stall the sequence. - That assumption fails for FreeRDP-based servers: GNOME Remote Desktop blocks in its `AWAIT_BW_RESULT` state until it receives a Bandwidth Measure Results reply and never proceeds to licensing, so the connection hangs right after the Client Info PDU. Windows servers tolerate the omission, which hid it. - Reply to a connect-time `BandwidthMeasureStop` with a `BandwidthMeasureResults` PDU carrying the payload size the server handed us, over a nominal interval. ## Scope This is the unblock alone. The reported interval is nominal (`time_delta_ms: 1`) because the sans-I/O layer has no time source: nothing reaches the connector that says when the bytes arrived. The figure is an informational QoS hint and the server proceeds on receipt, which is what unsticks the connection. Measuring it properly needs an arrival time threaded down from the I/O driver, which is a breaking change to `Sequence::step` and does not belong in a fix aimed at getting FreeRDP servers connecting. It is #1530, stacked on this branch: it introduces `MonotonicInstant`, has `Framed` record when each read completed, and replaces the nominal figure here with the real Start-to-Stop interval and accumulated byte count. Split out at @CBenoit's suggestion in review. ## Validation - `cargo xtask check fmt/typos/lints/tests/locks` all pass on the pinned toolchain. - Regression test: a connect-time Bandwidth Measure Stop produces a response frame and the auto-detect phase continues. - Reproduced and fixed live against gnome-remote-desktop 49: before, the connector stalled after Client Info with grd in `AWAIT_BW_RESULT`; after, grd proceeds through licensing and DEMAND_ACTIVE to an active session. ## Notes - Found while bringing up the client-side Graphics Pipeline against grd (#1446), but it is an independent connector bug affecting any connection to a FreeRDP-based server, not specific to EGFX. - Previously depended on #1511 for a zero-`payloadLength` regression test. #1511 has merged, and that test moved out with the rest of the measurement work, so there is no dependency left here.
88d6870 to
08107e1
Compare
There was a problem hiding this comment.
Pull request overview
Introduces driver-observed PDU arrival timestamps for accurate connect-time bandwidth measurement.
Changes:
- Adds
MonotonicInstantand updates the breakingSequence::stepAPI. - Records read completion times in async and blocking drivers.
- Measures bandwidth windows and adds regression tests.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
ffi/src/connector/mod.rs |
Adapts FFI connector stepping. |
ffi/src/connector/activation.rs |
Adapts FFI activation stepping. |
crates/ironrdp-web/src/session.rs |
Supplies an inert browser timestamp. |
crates/ironrdp-testsuite-core/tests/session/connection_activation.rs |
Updates activation tests. |
crates/ironrdp-testsuite-core/tests/server/acceptor.rs |
Updates acceptor tests. |
crates/ironrdp-testsuite-core/tests/connector/autodetect.rs |
Tests bandwidth measurement behavior. |
crates/ironrdp-connector/src/license_exchange.rs |
Accepts arrival timestamps. |
crates/ironrdp-connector/src/lib.rs |
Defines the timestamp type and API. |
crates/ironrdp-connector/src/connection.rs |
Implements bandwidth measurement. |
crates/ironrdp-connector/src/connection_finalization.rs |
Accepts arrival timestamps. |
crates/ironrdp-connector/src/connection_activation.rs |
Propagates arrival timestamps. |
crates/ironrdp-connector/src/channel_connection.rs |
Accepts arrival timestamps. |
crates/ironrdp-client/src/rdp.rs |
Updates manual sequence stepping. |
crates/ironrdp-blocking/src/framed.rs |
Records blocking-read timestamps. |
crates/ironrdp-blocking/src/connector.rs |
Passes blocking-read timestamps. |
crates/ironrdp-async/src/framed.rs |
Records and forwards async-read timestamps. |
crates/ironrdp-acceptor/src/finalization.rs |
Accepts arrival timestamps. |
crates/ironrdp-acceptor/src/connection.rs |
Propagates arrival timestamps. |
crates/ironrdp-acceptor/src/channel_connection.rs |
Accepts arrival timestamps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
08107e1 to
7d2ce5d
Compare
`cargo-semver-checks` could be unavailable while the public API compatibility job still passed, hiding real API breaks such as PR #1530. Install the checked binary in a run-scoped directory accessible to the unprivileged analysis user, and make setup or any unexpected semver exit status fail the job. Only cargo-semver-checks' documented results remain accepted: `0` for compatible and `100` for an incompatibility. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
This pull request is Please split it into focused pull requests that can each be reviewed on their own. When the parts build on each other, stacked pull requests let you open each one on top of the last without waiting for the one below to merge. Stacks require every branch to live in this repository, so from a fork, please open separate pull requests instead. Automated review resumes once the change is below the |
…-time The Network Characteristics Byte Count store was incremented by the payload length alone. [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in the payloadLength field plus the size of the header fields (8 bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop, so every counted message was reported 8 bytes short. 2.2.14.1.3 pins that 8 by requiring headerLength to be 0x08. Those two increments are the whole of the connect-time byte count. The section's other rule, which counts every byte received while the window is open, belongs to the 0x0014 and 0x0114 Starts. Those are the reliable and lossy UDP variants; connect-time is 0x1014 and its step list has no such clause. The Start and Stop arms now check the request type instead of matching every variant. That is required rather than tidy: 2.2.14.1.4 sets headerLength to 0x08 only for the 0x002B Stop and 0x06 otherwise, so a fixed 8-byte addend is correct only once the type is known. It also stops a continuous-detection request being answered with a connect-time result on the wrong channel, and 3.2.5.14 asks for a sequence-number correlation on the 0x0629 Stop that this phase does not track. This deliberately does not follow FreeRDP, which counts the whole PDU length at the framing layer in rdp.c after rdp_read_header, for any window including connect-time, then adds payloadLength again in autodetect.c. On this path that counts the payload twice and adds framing bytes the spec does not ask for. The figure is an informational QoS hint and the server proceeds either way, so following the spec costs no interop. Four existing expectations move by 8 per counted message: 4096 to 4104, 1536 to 1552, 3584 to 3608, 512 to 520. Two tests added for the request-type guards. Stacked on Devolutions#1530, which introduces the window state this counts into.
Two gaps the deep reviewer named on Devolutions#1530 that were left open. A second Bandwidth Measure Start has to clear both stores and restart the timer per MS-RDPBCGR 3.2.5.14. That was the one branch the spec explicitly requires and nothing exercised it; the new test fails independently if either half of the reset is dropped. Nothing pinned Framed::last_read_at either. The connector tests feed Sequence::step hand-picked instants, so a regression that stopped stamping reads would leave them green while every measured window collapsed to the unmeasurable floor. These cover both directions: a stamp that stops advancing, and a stamp taken on drain rather than on the read, which would break the rule that a PDU served from the buffer keeps the arrival time of the read that filled it. They live in ironrdp-testsuite-core rather than inline because ironrdp-async sets [lib] test = false, so an inline module would compile and never run.
650ebdc to
5157e46
Compare
…-time The Network Characteristics Byte Count store was incremented by the payload length alone. [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in the payloadLength field plus the size of the header fields (8 bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop, so every counted message was reported 8 bytes short. 2.2.14.1.3 pins that 8 by requiring headerLength to be 0x08. Those two increments are the whole of the connect-time byte count. The section's other rule, which counts every byte received while the window is open, belongs to the 0x0014 and 0x0114 Starts. Those are the reliable and lossy UDP variants; connect-time is 0x1014 and its step list has no such clause. The Start and Stop arms now check the request type instead of matching every variant. That is required rather than tidy: 2.2.14.1.4 sets headerLength to 0x08 only for the 0x002B Stop and 0x06 otherwise, so a fixed 8-byte addend is correct only once the type is known. It also stops a continuous-detection request being answered with a connect-time result on the wrong channel, and 3.2.5.14 asks for a sequence-number correlation on the 0x0629 Stop that this phase does not track. This deliberately does not follow FreeRDP, which counts the whole PDU length at the framing layer in rdp.c after rdp_read_header, for any window including connect-time, then adds payloadLength again in autodetect.c. On this path that counts the payload twice and adds framing bytes the spec does not ask for. The figure is an informational QoS hint and the server proceeds either way, so following the spec costs no interop. Four existing expectations move by 8 per counted message: 4096 to 4104, 1536 to 1552, 3584 to 3608, 512 to 520. Two tests added for the request-type guards. Stacked on Devolutions#1530, which introduces the window state this counts into.
|
Answering the size note. The three commits here do not separate into reviewable pull requests. The first The note suggests stacked pull requests and adds that they require every branch The body now says this under "Why this is one PR". It also now records that |
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you for the follow up
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
I would like to address the comment I added here, so we actually support that for WASM as well.
If we can actually get a MonotonicTime on all platforms, we can maybe consider removing the Option as well?
Also perform a pass of test minimization if you can
|
Both remaining points, since neither has a thread. Removing the What web-time does change is what Test minimization: 10 down to 8, no assertion lost. I kept both floor tests, which look like duplicates and are not: in |
Two gaps the deep reviewer named on Devolutions#1530 that were left open. A second Bandwidth Measure Start has to clear both stores and restart the timer per MS-RDPBCGR 3.2.5.14. That was the one branch the spec explicitly requires and nothing exercised it; the new test fails independently if either half of the reset is dropped. Nothing pinned Framed::last_read_at either. The connector tests feed Sequence::step hand-picked instants, so a regression that stopped stamping reads would leave them green while every measured window collapsed to the unmeasurable floor. These cover both directions: a stamp that stops advancing, and a stamp taken on drain rather than on the read, which would break the rule that a PDU served from the buffer keeps the arrival time of the read that filled it. They live in ironrdp-testsuite-core rather than inline because ironrdp-async sets [lib] test = false, so an inline module would compile and never run.
5157e46 to
a757203
Compare
…-time The Network Characteristics Byte Count store was incremented by the payload length alone. [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in the payloadLength field plus the size of the header fields (8 bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop, so every counted message was reported 8 bytes short. 2.2.14.1.3 pins that 8 by requiring headerLength to be 0x08. Those two increments are the whole of the connect-time byte count. The section's other rule, which counts every byte received while the window is open, belongs to the 0x0014 and 0x0114 Starts. Those are the reliable and lossy UDP variants; connect-time is 0x1014 and its step list has no such clause. The Start and Stop arms now check the request type instead of matching every variant. That is required rather than tidy: 2.2.14.1.4 sets headerLength to 0x08 only for the 0x002B Stop and 0x06 otherwise, so a fixed 8-byte addend is correct only once the type is known. It also stops a continuous-detection request being answered with a connect-time result on the wrong channel, and 3.2.5.14 asks for a sequence-number correlation on the 0x0629 Stop that this phase does not track. This deliberately does not follow FreeRDP, which counts the whole PDU length at the framing layer in rdp.c after rdp_read_header, for any window including connect-time, then adds payloadLength again in autodetect.c. On this path that counts the payload twice and adds framing bytes the spec does not ask for. The figure is an informational QoS hint and the server proceeds either way, so following the spec costs no interop. Four existing expectations move by 8 per counted message: 4096 to 4104, 1536 to 1552, 3584 to 3608, 512 to 520. Two tests added for the request-type guards. Stacked on Devolutions#1530, which introduces the window state this counts into.
I think the alternative of extending the FFI surface is the right move. The FFI surface there was already closely modeled after the Rust interface, so it would be natural we extend it the same way: a MonotonicTime wrapper + a way to produce a "now" value, then pass it through the step function. We gain feature parity, as we should, and at this point the question becomes: why bother supporting the alternative path where we don’t know the frame arrival time ( |
Two gaps the deep reviewer named on Devolutions#1530 that were left open. A second Bandwidth Measure Start has to clear both stores and restart the timer per MS-RDPBCGR 3.2.5.14. That was the one branch the spec explicitly requires and nothing exercised it; the new test fails independently if either half of the reset is dropped. Nothing pinned Framed::last_read_at either. The connector tests feed Sequence::step hand-picked instants, so a regression that stopped stamping reads would leave them green while every measured window collapsed to the unmeasurable floor. These cover both directions: a stamp that stops advancing, and a stamp taken on drain rather than on the read, which would break the rule that a PDU served from the buffer keeps the arrival time of the read that filled it. They live in ironrdp-testsuite-core rather than inline because ironrdp-async sets [lib] test = false, so an inline module would compile and never run.
…-time The Network Characteristics Byte Count store was incremented by the payload length alone. [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in the payloadLength field plus the size of the header fields (8 bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop, so every counted message was reported 8 bytes short. 2.2.14.1.3 pins that 8 by requiring headerLength to be 0x08. Those two increments are the whole of the connect-time byte count. The section's other rule, which counts every byte received while the window is open, belongs to the 0x0014 and 0x0114 Starts. Those are the reliable and lossy UDP variants; connect-time is 0x1014 and its step list has no such clause. The Start and Stop arms now check the request type instead of matching every variant. That is required rather than tidy: 2.2.14.1.4 sets headerLength to 0x08 only for the 0x002B Stop and 0x06 otherwise, so a fixed 8-byte addend is correct only once the type is known. It also stops a continuous-detection request being answered with a connect-time result on the wrong channel, and 3.2.5.14 asks for a sequence-number correlation on the 0x0629 Stop that this phase does not track. This deliberately does not follow FreeRDP, which counts the whole PDU length at the framing layer in rdp.c after rdp_read_header, for any window including connect-time, then adds payloadLength again in autodetect.c. On this path that counts the payload twice and adds framing bytes the spec does not ask for. The figure is an informational QoS hint and the server proceeds either way, so following the spec costs no interop. Four existing expectations move by 8 per counted message: 4096 to 4104, 1536 to 1552, 3584 to 3608, 512 to 520. Two tests added for the request-type guards. Stacked on Devolutions#1530, which introduces the window state this counts into.
Connect-time bandwidth measurement needs to know when bytes arrived, and nothing in the sans-I/O layer could tell it. The connector answered a Bandwidth Measure Stop with a fabricated interval because the real one was not observable from where the response is built. Introduce MonotonicInstant, a millisecond counter with no epoch and no platform dependency, and make it 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. Callers that genuinely have no frame to time (the no-input path) use step_no_input. With arrival times available, the connector measures 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. A Stop with no preceding Start still gets its response, because the server blocks without one, but reports a zero interval rather than inventing a figure. BREAKING CHANGE: Sequence::step takes a MonotonicInstant. Implementors must add the parameter; callers driving a sequence by hand must supply an arrival time or switch to step_no_input.
… was The connect-time Bandwidth Measure handler opened a counting window on every Start and reported the accumulated Payload bytes on the Stop, whatever the elapsed time turned out to be. On a driver that reports no arrival times the elapsed time is always zero and falls back to the 1 ms floor, so the reply paired a full byte count with an interval the client never measured. A server computing byteCount * 8 / timeDelta then scales its estimate with however much payload it chose to send, and the wasm32 and FFI drivers are in that position for the whole connection. Model the absence of a reading as the absence of a reading: Sequence::step takes Option<MonotonicInstant>, and MonotonicInstant::ZERO is gone. It had served as both the clock's origin and the "did not measure" sentinel, which is what made the two cases indistinguishable at the point where it mattered. A window now opens only when the Start carried a reading, so the two outcomes separate. A driver with no clock opens nothing, accumulates nothing, and answers the Stop with its own payload alone. A window that was timed reports every byte per MS-RDPBCGR 3.2.5.14, including when the elapsed time rounds down to the floor because one socket read delivered the whole exchange: the bytes did arrive inside that millisecond, so the floor bounds a real measurement. Framed::last_read_at becomes Option<MonotonicInstant> and the wasm arm of monotonic_now returns None rather than a fixed instant.
Two gaps the deep reviewer named on Devolutions#1530 that were left open. A second Bandwidth Measure Start has to clear both stores and restart the timer per MS-RDPBCGR 3.2.5.14. That was the one branch the spec explicitly requires and nothing exercised it; the new test fails independently if either half of the reset is dropped. Nothing pinned Framed::last_read_at either. The connector tests feed Sequence::step hand-picked instants, so a regression that stopped stamping reads would leave them green while every measured window collapsed to the unmeasurable floor. These cover both directions: a stamp that stops advancing, and a stamp taken on drain rather than on the read, which would break the rule that a PDU served from the buffer keeps the arrival time of the read that filled it. They live in ironrdp-testsuite-core rather than inline because ironrdp-async sets [lib] test = false, so an inline module would compile and never run.
`web-time` is `std::time` on every target except `wasm32-unknown-unknown`, where it reads `Performance.now()`, so the driver clock no longer has a hole in the browser build and `monotonic_now` is one function again rather than two behind a `cfg`. The crate is already in `Cargo.lock` through quinn and rustls-pki-types, so this is an edge and not a new dependency. The `Option` stays. `ffi/src/connector/` does not use `Framed`: the embedder owns the read loop and hands `step` bytes it has already read, so a clock read there would stamp when the caller called rather than when the bytes arrived, which is the error this series exists to remove. `MonotonicInstant`'s doc said the absence was about wasm; it now says what it is actually about. Test pass: ten to eight, no assertion lost. `connect_time_bandwidth_reports_the_measured_interval_and_total_bytes` was subsumed by `..._measured_window_reports_every_payload`, which asserts the same interval and also covers accumulation. `nothing_read_yet_means_no_arrival_time` survives as the opening assertion of `each_socket_read_advances_the_arrival_time`.
a757203 to
ca75ae3
Compare
…-time The Network Characteristics Byte Count store was incremented by the payload length alone. [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in the payloadLength field plus the size of the header fields (8 bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop, so every counted message was reported 8 bytes short. 2.2.14.1.3 pins that 8 by requiring headerLength to be 0x08. Those two increments are the whole of the connect-time byte count. The section's other rule, which counts every byte received while the window is open, belongs to the 0x0014 and 0x0114 Starts. Those are the reliable and lossy UDP variants; connect-time is 0x1014 and its step list has no such clause. The Start and Stop arms now check the request type instead of matching every variant. That is required rather than tidy: 2.2.14.1.4 sets headerLength to 0x08 only for the 0x002B Stop and 0x06 otherwise, so a fixed 8-byte addend is correct only once the type is known. It also stops a continuous-detection request being answered with a connect-time result on the wrong channel, and 3.2.5.14 asks for a sequence-number correlation on the 0x0629 Stop that this phase does not track. This deliberately does not follow FreeRDP, which counts the whole PDU length at the framing layer in rdp.c after rdp_read_header, for any window including connect-time, then adds payloadLength again in autodetect.c. On this path that counts the payload twice and adds framing bytes the spec does not ask for. The figure is an informational QoS hint and the server proceeds either way, so following the spec costs no interop. Four existing expectations move by 8 per counted message: 4096 to 4104, 1536 to 1552, 3584 to 3608, 512 to 520. Two tests added for the request-type guards. Stacked on Devolutions#1530, which introduces the window state this counts into.
Summary
MonotonicInstant, a millisecond counter with an arbitrary epoch, and makeOption<MonotonicInstant>a required parameter ofSequence::step. The I/O drivers already know when a read completed, soFramedrecords the arrival time of each read and hands it to the state machine. A driver with no clock passesNone.#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 Benoît Cortier (@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.
std::time::Instant::nowpanics onwasm32-unknown-unknown, andironrdp-connectorcompiles for it.ironrdp-webreachesironrdp-asyncthroughironrdp-futures, so the clock read iscfg'd there too: the wasm arm returnsNonerather than panicking on the first socket read.More fundamentally, a sequence that timestamps its own entry measures how fast it drained an already-filled buffer, not how long the bytes took to arrive.
Framedstamps the actual socket read, so a PDU served entirely frombufcorrectly reports the read that filled it.Why the parameter is an Option
Three situations look alike at the Stop and are not alike, and collapsing them produces a worse figure than the nominal one #1465 sends.
A Stop with no preceding Start has no window to time. A window whose Start, Payload and Stop arrived in one socket read carries the same instant on all three, so the elapsed time rounds down to zero; TCP coalescing makes this the common case on a fast link, because the connect-time payload is small. And a driver with no clock reports nothing at any point, which is not a measurement of zero but the absence of one.
Only the middle case is a measurement. Making the parameter
Option<MonotonicInstant>is what keeps it distinguishable: a window opens only when the Start carried a reading, so a driver with no clock opens none, accumulates nothing, and answers the Stop with its own payload alone. A window that was timed reports every byte per [MS-RDPBCGR] 3.2.5.14, including when the elapsed time floors to 1 ms, because the bytes really did arrive inside that millisecond.Both untimed cases report a 1 ms floor rather than 0, since a server computing
byteCount * 8 / timeDeltadivides by it.There is no
MonotonicInstant::ZERO. An earlier revision used it as both the clock's origin and the "did not measure" sentinel, which is precisely what made the wasm32 and FFI drivers indistinguishable from a fast link: they would have paired a full byte count with an interval the client never measured, and the reported bandwidth would have grown with however much payload the server chose to send.Breaking change
Sequence::steptakes anOption<MonotonicInstant>. Implementors must add the parameter; callers driving a sequence by hand must supply an arrival time or switch tostep_no_input, which passesNonefor them. All 8 in-tree implementors and the call sites acrossironrdp-acceptor,ironrdp-async,ironrdp-blocking,ironrdp-client,ironrdp-webandffiare updated here.Framed::last_read_atreturnsOption<MonotonicInstant>.Stacked on this
#1559 is stacked on this pull request and carries its three commits, so the merge order is this one then #1559. Landing this shrinks #1559's diff to its own change, 2 files and +149/-15 in
connection.rsplus one connector test.#1559 counts the 8-byte auto-detect header into the byte count introduced here, and narrows the Start and Stop arms to the connect-time request type. Both come from the automated reviewer's findings on this pull request.
Why this is one PR
The automation flags this as size/XL and asks for it to be split. The three commits here do not separate into reviewable pull requests.
The first changes the
Sequence::stepsignature, and every implementor and caller moves with it: connector, acceptor, async, blocking, client, web, the FFI and the tests. That is 19 of the 23 files and +474/-116 on its own, and a pull request holding the trait change without them does not build.The second exists because of the review on the first. It distinguishes a window that was never timed from one measured as zero, which is the fix for the finding that a wasm or FFI client would otherwise report a fabricated bandwidth. Splitting it out means filing the first commit with a defect already raised against it.
The third is the tests for both.
Stacked pull requests are the remedy the note suggests, and it adds that they require every branch to live in this repository. This branch is on a fork, so that path is closed.
Validation
cargo xtask check fmt/typos/lints/tests/locksall pass on the pinned toolchain,fuzz/built before the lock check.cargo xtask wasm checkpasses. Worth stating separately: this is the gate that catches theInstant::nowhazard above, since the workspace default target never compiles that crate for wasm.crates/ironrdp-testsuite-core/tests/connector/autodetect.rs: the interval and byte count match what the driver observed across a Start/Payload/Stop window; several Payload messages accumulate across a timed window; a window delivered in one read floors the interval with every byte still counted; a driver reporting no arrival times answers with the Stop payload alone; a Stop with no preceding Start is answered with the floor; and a second Start clears both stores and restarts the timer, which 3.2.5.14 requires and which nothing else covered.crates/ironrdp-testsuite-core/tests/async_framed.rscoveringFramed::last_read_atitself, since the tests above feedSequence::stephand-picked instants and would stay green if the driver stopped stamping reads. They pin that an unreadFramedreports no arrival, that each socket read advances the stamp, that PDUs sharing a read share its stamp rather than being stamped when the caller drains them, and that aFramedrebuilt around leftover bytes reports no arrival for them, which is the caseironrdp-tokio's split and unsplit helpers produce. Each was checked by planting the corresponding regression and confirming the test fails.ironrdp-testsuite-corerather than inline becauseironrdp-asyncsets[lib] test = false, so an inline#[cfg(test)]module would compile and never run. This addsironrdp-asyncas a dev-dependency ofironrdp-testsuite-core.