Skip to content

transport: ACK Frequency extension — ACK_FREQUENCY (0xaf) + IMMEDIATE_ACK (0x1f)#242

Merged
ch4r10t33r merged 1 commit into
masterfrom
feat/187-ack-frequency
Jul 10, 2026
Merged

transport: ACK Frequency extension — ACK_FREQUENCY (0xaf) + IMMEDIATE_ACK (0x1f)#242
ch4r10t33r merged 1 commit into
masterfrom
feat/187-ack-frequency

Conversation

@ch4r10t33r

Copy link
Copy Markdown
Collaborator

Closes #187.

Summary

Implements draft-ietf-quic-ack-frequency: the min_ack_delay transport parameter (0xff04de1b), the ACK_FREQUENCY frame (0xaf), and the IMMEDIATE_ACK frame (0x1f). quinn / quiche / msquic all ship this; until now those peers saw zquic as extension-less and fell back to the RFC 9000 baseline.

What's in

  • src/frames/ack_frequency.zig (new): frame codec + unit tests. Note 0xaf > 0x3f → 2-byte varint type on the wire (0x40 0xaf); both 1-RTT frame loops already varint-decode types so dispatch is clean.
  • Transport parameter: advertised by default at 1000 µs (1 ms = our drive-tick ACK granularity, well under the 25 ms max_ack_delay we advertise). TransportParamsOpts.min_ack_delay_us = 0 disables. Peer's value is parsed and stored; a peer advertising min_ack_delay > max_ack_delay is defensively treated as not-advertised rather than tearing down the handshake from inside the void TP-apply path (draft says TRANSPORT_PARAMETER_ERROR — noted inline).
  • Frame handling (server + client loops):
    • IMMEDIATE_ACK → flush the pending app-ACK now.
    • ACK_FREQUENCY → sequence-gated apply (stale/duplicate ignored per draft §4); malformed body → FRAME_ENCODING_ERROR close; requested max ack delay below our advertised min_ack_delay → PROTOCOL_VIOLATION close.
  • ACK cadence honor: zero default-path change — with no ACK_FREQUENCY received, ACKs flush every drive tick exactly as before. Once armed, flushAllConnAppAcks holds a conn's ACK until the ack-eliciting threshold is crossed, the requested delay expires, a reorder beyond the reordering threshold occurs, or IMMEDIATE_ACK arrives. The interop client keeps its eager per-batch flush (acking more often than requested is explicitly permitted by the draft).
  • PTO probes on both sides ride an IMMEDIATE_ACK next to the PING when the peer advertised support — probes get answered without waiting out the peer's (possibly relaxed) delayed-ack timer.

Not in (deliberate)

Sending ACK_FREQUENCY to tune the peer's cadence (an embedder-facing API). Receive-side honor is the interop-relevant half; the send half can follow when an embedder wants it.

Tests

  • Frame codec: round-trip, truncation at every cut point, IMMEDIATE_ACK single byte.
  • TP: round-trip default, omit-when-zero.
  • ConnState: apply/stale/protocol-violation gating; threshold + delay-timer + IMMEDIATE_ACK flush gating; reorder trigger incl. threshold=0 disable and µs→ms round-up.
  • 296/297 unit tests pass (1 pre-existing skip).
  • Local interop smoke (docker + quic-interop-runner): handshake + transfer green with both endpoints advertising the new TP.

)

Implements draft-ietf-quic-ack-frequency so peers (quinn / quiche /
msquic all ship it) can tune our ACK cadence:

- src/frames/ack_frequency.zig (new): ACK_FREQUENCY (0xaf) parse/
  serialize — sequence number, ack-eliciting threshold, requested max
  ack delay (µs), reordering threshold — and IMMEDIATE_ACK (0x1f).
  0xaf > 0x3f so the type is a 2-byte varint on the wire; both frame
  loops already varint-decode the type.

- quic_tls.zig: advertise + parse the min_ack_delay transport parameter
  (0xff04de1b, MICROSECONDS). Advertised by default at 1000 µs (1 ms —
  our drive-tick ACK granularity; well under the 25 ms max_ack_delay we
  advertise). Set TransportParamsOpts.min_ack_delay_us = 0 to disable.

- io.zig frame loops (server + client): 0x1f sets ack_immediate; 0xaf is
  sequence-gated (stale frames ignored per draft §4), malformed bodies →
  FRAME_ENCODING_ERROR, requested delay below our advertised
  min_ack_delay → PROTOCOL_VIOLATION.

- ACK cadence: with no ACK_FREQUENCY received the flush path is
  UNCHANGED (every drive tick). Once armed, flushAllConnAppAcks holds a
  conn's ACK until the eliciting-packet threshold is crossed, the
  requested max ack delay expires, a reorder event beyond the reordering
  threshold occurs, or IMMEDIATE_ACK arrives. Ack-eliciting counting
  piggybacks on noteFrameReceived (anything but PADDING/ACK/
  CONNECTION_CLOSE). The interop client keeps its eager per-batch flush
  — acking more often than requested is explicitly permitted by the
  draft.

- PTO probes (both sides) ride an IMMEDIATE_ACK alongside the PING when
  the peer advertised min_ack_delay, so probes are answered without
  waiting out the peer's (possibly relaxed) delayed-ack timer.

- Peer TP validation: min_ack_delay > max_ack_delay is treated
  defensively as not-advertised (extension disabled for that peer)
  rather than tearing down the handshake from the void apply path.

Tests: frame codec round-trip + truncation, TP round-trip + omit-when-
zero, ConnState apply/stale/violation gating, threshold + delay-timer +
IMMEDIATE_ACK flush gating, reorder trigger incl. threshold=0 disable.
296/297 pass (1 pre-existing skip). Local interop smoke: handshake +
transfer green with both endpoints advertising the new TP.
@ch4r10t33r ch4r10t33r merged commit 6a14bd2 into master Jul 10, 2026
10 checks passed
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.

draft-ietf-quic-ack-frequency: ACK_FREQUENCY (0xAF) + IMMEDIATE_ACK (0x1F) frames not implemented

1 participant