You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)