Skip to content

Indicate the datagram send state to server applications - #6179

Open
Masahiro Kozuka (masa-koz) wants to merge 1 commit into
microsoft:mainfrom
masa-koz:masa-koz/datagram-state-server-indication
Open

Indicate the datagram send state to server applications#6179
Masahiro Kozuka (masa-koz) wants to merge 1 commit into
microsoft:mainfrom
masa-koz:masa-koz/datagram-state-server-indication

Conversation

@masa-koz

Copy link
Copy Markdown
Contributor

Description

A server application never receives QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED unless path MTU discovery happens to change the max send length. On a path where it does not, the application waits forever for the event and never sends a datagram, while receiving them perfectly well. docs/API.md states that the app receives this event to learn whether the peer supports receiving datagrams, so this is the code not matching the documented behaviour.

Cause

QuicDatagramOnSendStateChanged commits the new state to the QUIC_DATAGRAM unconditionally, but indicates it only when State.ExternalOwner is set:

    if (SendEnabled == Datagram->SendEnabled) {
        if (!SendEnabled || NewMaxSendLength == Datagram->MaxSendLength) {
            return;                                   // "already reported"
        }
    }

    Datagram->MaxSendLength = NewMaxSendLength;        // recorded regardless

    if (Connection->State.ExternalOwner) {             // reported only if owned
        ... QuicConnIndicateEvent(DATAGRAM_STATE_CHANGED) ...
    }

On a server the peer's transport parameters are processed before the listener hands the connection to the application. That evaluation runs unowned: it records a state that nobody can be told about, and from then on every evaluation compares equal to it and returns early. The change has been accounted for as if it were reported.

The only rescue is a later evaluation computing a different length, which in practice means MTU discovery moving Path->Mtu. Hence the same build working on one network and hanging on another.

Fix

QuicConnSetConfiguration evaluates the send state for servers, right after State.Started is set. That is the first point at which both inputs to the state are settled and there is an owner to indicate the result to — Started selects which MTU the max send length is derived from (QUIC_DPLPMTUD_MIN_MTU before it, Path->Mtu after).

That alone is not enough, because the evaluation is still swallowed whenever the recomputed values happen to equal what was recorded while unowned. They are identical when the peer uses an 8 byte connection ID over an IPv6 path with a 1280 byte MTU, which is exactly the pre-Started assumption. So whether the live state moved and whether the application's view of it is stale are now tracked separately: the early return consults the indicated state, the indicated state is updated only where an indication is actually made, and the side effects of a real state change — send shutdown, max length requeue — stay tied to the live state.

The indicated state starts out as what the application can be assumed to know already. A client opens the connection itself, so it knows the initial state; a server is handed a connection whose send state is settled before it ever sees it, so it knows nothing and assumes the conservative default of not being able to send.

Scope of the behaviour change

  • Clients are unaffected. Their send state changes always have an owner to indicate to, so the indicated state never goes stale and every existing indication still fires, unchanged.
  • Servers gain exactly one indication, telling the application once that it may send datagrams.
  • A server whose peer does not support datagrams is still told nothing, which matches what the application already assumes. No spurious event is introduced.
  • QuicDatagramValidate's !SendEnabled ⇒ MaxSendLength == 0 invariant holds on every path.
  • QUIC_DATAGRAM is internal to src/core, so the two added fields are not an API or ABI change.

Testing

QuicTestDatagramNegotiation covered this path but only asserted that the server can send (GetParam(QUIC_PARAM_CONN_DATAGRAM_SEND_ENABLED)), which was true even while the application was never told. It now also asserts that the server was told: TestConnection records the count and contents of the DATAGRAM_STATE_CHANGED events indicated to it.

  • Without the fix, the extended test fails on all four parameterisations (v4/v6 × datagram receive enabled/disabled).
  • With the fix, the full *Datagram*:*Event*:*Mtu*:*Resumption*:*ValidateConn*:*Basic*:*Handshake* set passes: 797 tests, no failures. In particular the ordered event validators in EventTest.cpp needed no changes, confirming client behaviour is untouched.

Documentation

No documentation change. docs/API.md and docs/api/QUIC_CONNECTION_EVENT.md already describe the behaviour this change implements.

A server application never receives QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED
unless path MTU discovery happens to change the max send length, so on a
path where it does not, the application waits forever and never sends a
datagram - while receiving them perfectly well.

QuicDatagramOnSendStateChanged committed the new state to the QUIC_DATAGRAM
unconditionally, but indicated it only when State.ExternalOwner was set. On
a server the peer's transport parameters are processed before the listener
hands the connection to the application, so that evaluation recorded a
state with nobody to report it to, and nothing re-evaluates afterwards. The
only rescue is a later evaluation that computes a different length, which
in practice means MTU discovery moving Path->Mtu - hence the same build
working on one network and hanging on another.

Fix it in two parts.

QuicConnSetConfiguration now re-evaluates the send state for servers, right
after State.Started is set. That is the first point at which a server
connection has an owner to indicate to, and Started is an input to the max
send length (derived from QUIC_DPLPMTUD_MIN_MTU before it and from the
path's MTU after), so the value has to be recomputed there in any case.

That alone would still be swallowed whenever the recomputed values happen
to match what was recorded while unowned - the peer using an 8 byte
connection ID over a 1280 byte path makes them identical. Whether the live
state moved and whether the application's view of it is stale are separate
questions, so QUIC_DATAGRAM now tracks both: the early return consults the
indicated state, the indicated state is updated only where an indication is
actually made, and the side effects of a real state change - send shutdown,
max length requeue - stay tied to the live state.

The indicated state starts out as whatever the application can be assumed
to know already. A client opens the connection itself and so knows the
documented initial state; a server is handed a connection whose send state
was settled before it ever saw it, and assumes it cannot send. This keeps
client behaviour identical and adds exactly one new indication: a server
learns, once, that it may send datagrams. A server whose peer does not
support them is still told nothing, matching what it already assumes.

QUIC_DATAGRAM is internal to core, so the added fields are not an API or
ABI change.

QuicTestDatagramNegotiation now checks that the server was told it can
send, rather than only that it can. TestConnection records the state
carried by the last DATAGRAM_STATE_CHANGED to make that observable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@masa-koz
Masahiro Kozuka (masa-koz) requested a review from a team as a code owner July 28, 2026 21:32
Masahiro Kozuka (masa-koz) added a commit to seera-networks/msquic that referenced this pull request Jul 29, 2026
Description
Brings this branch in line with the upstream version of the #47 fix, microsoft#6179.

#47 fixed the real bug — a server application is never told it may send datagrams unless path MTU discovery happens to change the max send length — but it changed client behaviour on the way. Preparing the same fix for upstream showed that the client-side part is neither needed nor desirable, so this narrows it.

What #47 did to clients
#47 re-evaluated the send state for every connection at QuicConnSetConfiguration, and started the indicated state at (FALSE, 0) for both roles. On a client that produces an extra DATAGRAM_STATE_CHANGED at ConnectionStart, carrying SendEnabled = TRUE before the peer's transport parameters are known. That indication is speculative: when the peer turns out not to support datagrams it is reversed a moment later. It also required changing thirteen ordered event expectations in EventTest.cpp, which is the test suite telling us the client's event sequence changed.

What this changes
Two changes make the fix server-only.

The re-evaluation at SetConfiguration runs only for servers. A client's send state changes always have an owner to indicate to, so its indicated state never goes stale and it needs nothing there.

The indicated state starts out as what the application can be assumed to know already, which differs by role. A client opens the connection itself, so it knows the documented initial state of (TRUE, UINT16_MAX). A server is handed a connection whose send state was settled before it ever saw it, so it knows nothing and assumes the conservative default of not being able to send.

Result
Client behaviour is bit-for-bit identical to before Track the datagram send state last indicated to the application #47.
Servers gain exactly one indication: the application learns, once, that it may send datagrams.
A server whose peer does not support datagrams is still told nothing, matching what it already assumes. No spurious event is introduced.
EventTest.cpp needs no changes, which is the check that client behaviour is untouched.
The core of #47 — tracking the state last indicated separately from the live state, so a change made while unowned is not mistaken for one already reported — is unchanged and still what makes the fix work when the recomputed values happen to match those recorded while unowned.

Testing
Also carried over from upstream: QuicTestDatagramNegotiation now checks that the server was told it can send, rather than only that it can. TestConnection records the count and contents of the DATAGRAM_STATE_CHANGED events indicated to it, since GetParam(QUIC_PARAM_CONN_DATAGRAM_SEND_ENABLED) reports the live state whether or not the application ever heard about it.

With the server re-evaluation removed, DatagramNegotiation fails on all four parameterisations, so the assertion covers the original bug.
*Datagram*:*Event*:*ValidateConn*:*Basic*:*Mtu*:*Resumption* passes in full: 529 tests, no failures. No compiler warnings.
Documentation
No documentation change. docs/API.md and docs/api/QUIC_CONNECTION_EVENT.md already describe the behaviour this implements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant