Skip to content

First-flight transport errors can make pending CONNECTION_CLOSE unsendable #2515

Description

@mihed

In quiche 0.29.1 and 0.29.2, a transport error detected while processing the peer’s first flight can be recorded as a local connection error, but then made unsendable before send() can emit a peer-processable CONNECTION_CLOSE.

One concrete case is the existing missing_initial_source_connection_id test. The server detects Error::InvalidTransportParam, records a local error, and then marks the connection closed because recv_count == 0. A later send() therefore returns without emitting the pending transport close.

Minimal reproduction

Extend the existing missing_initial_source_connection_id test in quiche/src/tests.rs:

assert_eq!(
    pipe.server_recv(&mut buf[..len]),
    Err(Error::InvalidTransportParam)
);
assert!(pipe.server.local_error().is_some());
assert!(pipe.server.send(&mut buf).is_ok());

Run:

cargo test -p quiche missing_initial_source_connection_id -- --nocapture

Observed behavior

On unmodified quiche 0.29.1 and 0.29.2:

  1. server_recv() returns Err(Error::InvalidTransportParam).
  2. server.local_error() is set.
  3. Connection::close() marks the connection closed because recv_count == 0.
  4. A subsequent send() cannot serialize the recorded close.

If the recv_count == 0 early-close path is relaxed, send() can still select a Handshake packet while the TLS write level maps to packet::Epoch::Application and the handshake is not confirmed. In this first-flight error case, Initial keys are still available, and selecting Handshake can fail instead of emitting an Initial CONNECTION_CLOSE.

Expected behavior

After the server has processed the Initial far enough to detect and record a transport-parameter connection error, send() should be able to emit a peer-processable transport CONNECTION_CLOSE, when Initial sealing keys are available.

For the missing_initial_source_connection_id case, the emitted close should be an Initial packet carrying a transport-level CONNECTION_CLOSE with error code TRANSPORT_PARAMETER_ERROR (0x08).

Minimal patch demonstrating the issue

diff quiche/src/lib.rs
-        if self.recv_count == 0 {
+        if self.recv_count == 0 && !self.derived_initial_secrets {
             self.mark_closed();
         }
 
             if !self.handshake_confirmed {
                 match epoch {
+                    packet::Epoch::Application
+                        if self.crypto_ctx[packet::Epoch::Initial].has_keys() =>
+                        return Ok(Type::Initial),
                     packet::Epoch::Application => return Ok(Type::Handshake),

With this change, the focused reproduction can emit an Initial transport CONNECTION_CLOSE instead of making the recorded local error unsendable.

This patch is intended as a minimal demonstration of the failing state transition, not necessarily as the final upstream design.

RFC basis

RFC 9000 §7.3 requires the initial_source_connection_id transport parameter to be present. If it is absent, the endpoint MUST close the connection with TRANSPORT_PARAMETER_ERROR.

RFC 9000 §18.2 defines server-only transport parameters. If a client sends a server-only transport parameter, the server MUST treat that as a connection error of type TRANSPORT_PARAMETER_ERROR.

RFC 9000 §17.2 requires nonzero reserved bits in a protected long-header packet, after packet and header protection are removed, to be treated as a connection error of type PROTOCOL_VIOLATION.

RFC 9000 §17.3.1 requires nonzero reserved bits in a protected short-header packet, after packet and header protection are removed, to be treated as a connection error of type PROTOCOL_VIOLATION.

Relevant RFC sections:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions