Skip to content

1.8.2

Latest

Choose a tag to compare

@benoitc benoitc released this 05 Sep 10:24
· 37 commits to main since this release

Added

  • versions lists the QUIC versions a connection will also accept, for
    RFC 9368 compatible version negotiation. Contributed by jbevemyr (#243).
  • hibernate_after (default 5000 ms) hibernates an idle connection
    process, running a fullsweep so the handshake's garbage stops being
    pinned to a heap that never collects on its own. infinity opts out.
    Contributed by jbevemyr (#207).
  • TLS secrets are written to the file named by SSLKEYLOGFILE when it is
    set, in the format Wireshark reads. Contributed by jbevemyr (#231).
  • max_burst_packets bounds how many packets leave per send drain, so a
    large queued write cannot monopolise the scheduler. Contributed by
    jbevemyr (#214).
  • ack_packet_tolerance makes the 1-RTT ACK decimation threshold
    configurable; it defaults to the RFC 9000 section 13.2.1 value of 2.
    Contributed by jbevemyr (#213).

Fixed

  • A stream written in more than one send_data/4 call no longer goes out
    interleaved. When the per-drain burst budget was spent, the unsent
    remainder was requeued at the back of its priority bucket instead of the
    front, so the drain round-robined between the queued entries and the
    stream stayed out of order for the rest of the transfer, leaving the
    receiver holding most of it for reassembly (1.2 MB buffered on a 2 MB
    transfer in 16 writes, against 0 once ordered). Throughput on loopback
    is unchanged; the cost is the reassembly buffer, and any real path where
    reordering matters.
  • An authenticated distribution connection no longer drops the peer's
    first handshake message about half the time. The auth_callback path
    put a short-lived gatekeeper process in front of the dist controller
    and replayed its mailbox after the handoff, but the controller took
    ownership from a gen_statem state-enter callback, which runs after
    start_link has already returned; everything the connection emitted in
    between arrived at a process that had stopped reading. The controller
    now owns the connection from the first packet on the server and from
    before start_link returns on the client, so there is no handoff to
    race. Auth callbacks must not open streams, which never worked.
  • A TLS alert raised during the handshake now reaches the peer. The
    CONNECTION_CLOSE was batched into the state send_tls_alert/2 returns,
    which the six immediate-exit sites discarded before calling exit/1;
    terminate/3 could not recover it, since it runs with the pre-alert
    state and its fallback close is skipped entirely while app keys do not
    exist. The peer saw silence and waited out its idle timeout instead of
    learning why the handshake failed. Reported by obi458 (#227).
  • Two nodes dialling each other at the same time no longer deadlock over
    QUIC distribution. net_kernel resolves a simultaneous connect by
    killing the losing setup process and then blocking, with no timeout,
    until that process dies; the setup process trapped exits, so the signal
    became an unread message, nothing died, and the node's distribution
    machinery stayed wedged until both dials timed out. It now acts on that
    exit while it still owns the connection, and stops trapping once the
    controller has taken ownership, which is what OTP's own setup processes
    do.
  • An HTTP/3 message body ends at stream end rather than at a frame
    boundary, so a response whose last DATA frame is followed by the FIN
    in a separate packet is not truncated. Contributed by jbevemyr (#244).
  • A server answers a long-header packet carrying a version it does not
    support with a Version Negotiation packet (RFC 9000 section 6.1)
    instead of silence. Probing with a reserved version is how readiness
    checks and the interop runner detect a live server. A packet that is
    itself a Version Negotiation is never answered. Contributed by
    jbevemyr (#229).
  • QUIC v2 (RFC 9369) uses the correct wire format: version-specific
    packet-type bits, salts and key labels, and compatible version
    negotiation settles on the first ClientHello. Contributed by
    jbevemyr (#243).
  • A client switches away from a connection ID the peer has retired
    (RFC 9000 section 5.1.2) instead of continuing to use it.
    Contributed by jbevemyr (#218).
  • The keep-alive PING no longer spins on a connection whose peer has
    gone quiet. Contributed by jbevemyr (#221).
  • An unresponsive peer is given up on after a disconnect timeout, checked
    on its own timer rather than on the PTO. Contributed by jbevemyr (#224).
  • A peer address change is followed immediately and validated in the
    background, so a NAT rebind does not stall the connection while
    validation runs. Contributed by jbevemyr (#255).
  • The server sends a Retry when configured to, and its cipher preference
    order is configurable rather than fixed. Contributed by jbevemyr (#232).
  • The client offers the cipher suites it was configured with instead of
    always the built-in list, and honours the version option.
    Contributed by jbevemyr (#232, #235).
  • Short headers are unprotected with the negotiated cipher, so a
    connection that negotiated ChaCha20-Poly1305 no longer fails to
    decrypt. Contributed by jbevemyr (#234).
  • A session ticket selects the PSK it belongs to rather than only
    feeding 0-RTT, so resumption works on its own. Contributed by
    jbevemyr (#238).
  • The session-ticket table is owned by the server registry rather than
    whichever connection created it: it used to vanish with that
    connection, and a client resuming afterwards fell back to a full
    handshake. Contributed by jbevemyr (#239).
  • Only the client sends 0-RTT, and 0-RTT data lost in flight is resent.
    Oversized 0-RTT writes are split, and the idle-state send path is
    gated too. Contributed by jbevemyr (#240).
  • The connection-level MAX_DATA update triggers on remaining headroom
    rather than cumulative bytes received. The old comparison became
    permanently true once total received passed one window, putting an
    ack-eliciting MAX_DATA on every packet. Contributed by jbevemyr (#209).
  • Retained ACK ranges are capped at 64 per packet-number space
    (RFC 9000 section 13.2.4). Under burst loss the list fragmented into
    hundreds of ranges, and every outgoing ACK encoded all of them.
    Contributed by jbevemyr (#211).
  • Overlapping and duplicate chunks are handled when reassembling stream
    and CRYPTO data, keeping the longer chunk at a given offset instead of
    trusting whichever arrived first. Contributed by jbevemyr (#223).
  • A flow-control-blocked write no longer strands data behind a blocked
    queue head: the sendable prefix goes out, the remainder requeues in
    order, and queued data is normalised to a binary. Contributed by
    jbevemyr (#233).
  • Streams are reclaimed when their FIN is acknowledged rather than when
    it is sent, so a lost FIN cannot retire the stream early. Contributed
    by jbevemyr (#236).
  • MAX_STREAM_DATA is no longer sent for a stream whose final size the
    peer has already declared. Contributed by jbevemyr (#241).
  • The client's retained Finished flight carries its own retransmission
    timer. Once the state machine leaves the handshake nothing is
    guaranteed to be in flight to arm a PTO, so a Finished lost more than
    once was never resent and the handshake stalled until the idle timeout.
  • The GSO segment size is derived from the batch instead of a
    configured constant. 1-RTT packets follow the current max datagram
    size, so the uniformity check against the fixed 1200 never matched and
    the GSO path never ran; a batch is now split into runs of equal-sized
    packets and each run segmented on its own size. Each write is capped
    at 64 segments and 64 KB, and GSO is requested per message rather than
    as a socket-level UDP_SEGMENT, which segmented every datagram
    including handshake packets. Reported by jbevemyr (#196).
  • The socket-backend client binds the source address from
    extra_socket_opts instead of leaving it to the kernel's route
    lookup, which picks the wrong address on a multi-address host.
    Contributed by jbevemyr (#261).
  • A GRO train reaches the client connection as one message rather than
    one per packet, so the whole train is processed in a single receive
    pass. Contributed by jbevemyr (#248).
  • PTO probe packets are exempt from the congestion window and loss
    retransmissions are bound to it, per RFC 9002 section 7. A probe is
    the only thing that can restart a stalled connection, so blocking it
    on a window the peer's silence keeps closed deadlocks the transfer;
    ordinary loss retransmissions, which were previously sent regardless,
    now respect the window like any other send. Contributed by jbevemyr
    (#249).
  • ACKs arriving at the Initial or Handshake encryption level no longer
    reach the 1-RTT loss tracker. Packet numbers restart per space
    (RFC 9000 §12.3), so a Handshake-space ACK of packet numbers 0..N was
    retiring the first N 1-RTT packets from the sent queue without the peer
    having received them: nothing retransmitted them and the peer kept a
    permanent hole in the stream. A path that drops a full window and then
    returns (WiFi-to-cellular handover, VPN reconnect, NAT rebind) left the
    transfer stalled for good. Contributed by jbevemyr (#250).
  • The sent-packet tracker now survives an active path migration. It was
    replaced wholesale, which orphaned every packet already in flight: no
    ACK matched them, loss detection never ran, and bytes_in_flight read
    zero so no PTO fired either. Their data was never retransmitted and
    the peer kept a permanent hole in the stream. The path-derived
    estimates (RTT, PTO count) still reset, since those belong to the old
    path. Contributed by jbevemyr (#251).
  • The PTO backoff is capped at 5 seconds. RFC 9002 section 6.2.1 doubles
    the PTO on each consecutive expiration and the doubling had no
    ceiling, so on a 50 ms path it reached roughly 90 seconds after nine
    expirations and about twelve minutes after twelve. A probe scheduled
    that far out never happens: the idle timer and any request deadline
    above it have long since fired. Contributed by jbevemyr (#254).
  • The loss time threshold is max(smoothed_rtt, latest_rtt)
    (RFC 9002 section 6.1.2) rather than the smoothed estimate alone. When
    an RTT spike outruns the EWMA the two diverge, and the smaller
    threshold declares in-flight packets lost while their ACKs are merely
    late. Each spurious loss both retransmits data the peer already has
    and collapses the congestion window, so a single latency excursion
    (receiver queueing, bufferbloat) turned into a throughput collapse.
    Contributed by jbevemyr (#210).
  • PMTU probes are tracked as non-ack-eliciting, so a probe lost past the path MTU no longer inflates bytes_in_flight, arms the PTO machinery, or feeds a congestion event (RFC 8899 §3, RFC 9000 §14.4). Combined with an in-flight-keyed liveness check, the periodic raise probe previously killed every long-lived connection on an MTU-limited path once per 600-second raise interval, both ends at once. The raise interval is configurable as pmtu_raise_interval. (#264)
  • The UDP_GRO control message is read as the int the kernel sends.
    Matching exactly two bytes meant the lookup never succeeded, so a
    GRO-coalesced buffer was passed up unsplit as one oversized datagram
    and dropped by the QUIC layer, which cannot re-split short-header
    packets. GRO was therefore silently losing every coalesced train.
    Contributed by jbevemyr (#204).
  • The GRO receive path sizes its read buffer for a maximally coalesced
    train (64 KiB). Passing 0 used the OTP default 8 KiB buffer and
    recvmsg' silently truncated anything larger, discarding every segment past the first few. The client receiver also went through a plain recvfrom', which never split trains at all. Contributed by jbevemyr
    (#215).
  • A client whose Finished is lost now recovers. The
    Certificate(+CertificateVerify)+Finished flight goes out at the
    Handshake level, and once the client state machine left `handshaking'
    nothing retransmitted it: handshake-space packets are not in the 1-RTT
    loss tracker and the handshake retransmit timer only runs in that
    state. The client considered itself connected and sent 1-RTT data the
    server could not act on before handshake completion, while the server
    replayed its own flight against ACK-only answers, until the connection
    died on the idle timer. The flight is now retained until
    HANDSHAKE_DONE and resent from the PTO and on a duplicate
    handshake-level CRYPTO at offset 0. A write that exceeds the peer's
    flow-control limit also sends the part that fits rather than nothing,
    which is what lets the recovered connection drain its queue.
    Contributed by jbevemyr (#252, #230, #225).
  • Anti-amplification accounting (RFC 9000 §8.1) now also runs on the batched listener delivery path. A server whose ClientHello arrived in a GRO batch kept its amp budget at zero, deferred the handshake flight, and the handshake wedged until the connect timeout. (#263)
  • A server handshake flight lost on the wire is retransmitted on the client-Initial backoff schedule until the client's Finished arrives. Initial/Handshake packets are not loss-tracked, so a lost flight previously wedged the handshake permanently: the client's Initial retransmits only elicited ACKs once the server TLS state had advanced. (#263)

Changed

  • Header protection reuses a cipher context instead of re-running the key
    schedule on every packet. Measured on an 8 MB transfer, the crypto:*
    share of connection-process time drops from 10.95% to 8.30%; header
    protection itself goes from 0.77us to 0.28us per mask. Both directions'
    keys are cached, since they alternate packet by packet.
  • The interop runner speaks real HTTP/3 in the http3 case, serves more
    than one request per connection, issues requests concurrently within
    the peer's stream credit, and its resumption and 0-RTT cases test what
    they claim. Contributed by jbevemyr (#245).
  • A mixed-size send batch on a GSO socket is split into runs of
    equal-sized packets and each run sent with one UDP_SEGMENT call,
    instead of falling back to one `sendmsg' per packet. A single
    odd-sized packet between data packets (an ACK, a flow-control update)
    previously degraded the whole batch. Contributed by jbevemyr (#217).
  • Small sends are coalesced into shared packets. Contributed by jbevemyr
    (#203).
  • The pacing burst allowance scales with the pacing rate rather than
    sitting at a fixed 12 packets. Pacing wakeups have roughly
    millisecond resolution, so the fixed bucket capped throughput at 12
    packets per wakeup whenever the sender outran the ACK clock,
    regardless of the configured rate. Contributed by jbevemyr (#219).
  • A server with no explicit groups option now offers every classical
    group the crypto layer supports (x25519, secp256r1, secp384r1) instead
    of x25519 alone. A preference list holding only x25519 sent a
    HelloRetryRequest to any client whose key_share led with another
    curve, costing a full extra round trip on every such connection and
    exercising the HRR path in flows that did not need it. picoquic shares
    P-256 first, so this was every connection from it. An explicit
    groups option still restricts the set exactly as before.
    Contributed by jbevemyr (#246).
  • The CONNECTION_CLOSE sent from terminate/3 (owner death, exit signals) reaches the wire: it was batched into an updated socket state that terminate discarded, flushing the stale one instead, so the peer never learned of the close and held a phantom connection until its idle timeout. Peers now see an owner-death close within milliseconds. (#265)