Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/crypto/quic_tls.zig
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ pub const TransportParamsOpts = struct {
grease_quic_bit: bool = false,
/// RFC 9221 `max_datagram_frame_size` (0x20). Omitted when zero (disabled).
max_datagram_frame_size: u64 = 0,
/// draft-ietf-quic-ack-frequency `min_ack_delay` (0xff04de1b), in
/// MICROSECONDS. Advertising it signals support for ACK_FREQUENCY /
/// IMMEDIATE_ACK frames and obligates us to honor them. Omitted when
/// zero. Default 1 ms — matches the drive-loop ACK granularity. Must
/// not exceed our advertised max_ack_delay (25 ms).
min_ack_delay_us: u64 = 1000,
};

/// Preset transport-parameter profiles for common embedders.
Expand Down Expand Up @@ -336,6 +342,10 @@ pub fn buildTransportParams(out: []u8, opts: TransportParamsOpts) (varint.Encode
if (opts.max_datagram_frame_size > 0) {
pos = try writeParamVarint(out, pos, 0x20, opts.max_datagram_frame_size);
}
// draft-ietf-quic-ack-frequency: min_ack_delay (0xff04de1b), microseconds.
if (opts.min_ack_delay_us > 0) {
pos = try writeParamVarint(out, pos, 0xff04de1b, opts.min_ack_delay_us);
}
return pos;
}

Expand Down Expand Up @@ -398,6 +408,9 @@ pub const PeerTransportParams = struct {
grease_quic_bit: bool = false,
/// 0x20 — RFC 9221 max DATAGRAM frame payload size. Zero when absent.
max_datagram_frame_size: u64 = 0,
/// 0xff04de1b — draft-ietf-quic-ack-frequency min_ack_delay in
/// MICROSECONDS. Zero when absent (peer does not support the extension).
min_ack_delay_us: u64 = 0,
};

/// On-wire layout of the preferred_address transport parameter (RFC 9000
Expand Down Expand Up @@ -494,6 +507,7 @@ pub fn parseTransportParams(bytes: []const u8) varint.DecodeError!PeerTransportP
0x0e => out.active_connection_id_limit = readVarintField(value) catch continue,
0x2ab2 => out.grease_quic_bit = (value_len == 0),
0x20 => out.max_datagram_frame_size = readVarintField(value) catch continue,
0xff04de1b => out.min_ack_delay_us = readVarintField(value) catch continue,
else => {}, // unknown / reserved / connection-id params are not surfaced here
}
}
Expand Down Expand Up @@ -527,6 +541,20 @@ test "transport params: round-trip varint fields" {
try testing.expectEqual(@as(u64, 4), parsed.active_connection_id_limit);
// We don't emit `disable_active_migration`; check the default here.
try testing.expectEqual(false, parsed.disable_active_migration);
// draft-ietf-quic-ack-frequency: advertised by default at 1 ms.
try testing.expectEqual(@as(u64, 1000), parsed.min_ack_delay_us);
}

test "transport params: min_ack_delay omitted when zero" {
const testing = std.testing;
var buf: [256]u8 = undefined;
const cid = [_]u8{0xaa};
const n = try buildTransportParams(&buf, .{
.initial_source_cid = &cid,
.min_ack_delay_us = 0,
});
const parsed = try parseTransportParams(buf[0..n]);
try testing.expectEqual(@as(u64, 0), parsed.min_ack_delay_us);
}

test "transport params: max_datagram_frame_size round-trip" {
Expand Down
144 changes: 144 additions & 0 deletions src/frames/ack_frequency.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//! QUIC ACK Frequency extension frames (draft-ietf-quic-ack-frequency).
//!
//! Two frames let a sender tune how often the peer acknowledges:
//! - ACK_FREQUENCY (0xaf): request an ack-eliciting threshold, a max ack
//! delay, and a reordering threshold.
//! - IMMEDIATE_ACK (0x1f): request an ACK right now (e.g. in PTO probes).
//!
//! Both are extension frames: an endpoint MUST NOT send them unless the peer
//! advertised the `min_ack_delay` transport parameter (0xff04de1b). Frame
//! type 0xaf is > 0x3f, so it occupies a 2-byte varint on the wire (0x40 0xaf);
//! 0x1f fits in one byte.

const std = @import("std");
const varint = @import("../varint.zig");

/// Frame type values (draft-ietf-quic-ack-frequency §4/§5 provisional codepoints).
pub const ack_frequency_frame_type: u64 = 0xaf;
pub const immediate_ack_frame_type: u64 = 0x1f;

/// `min_ack_delay` transport parameter id (draft §3). Value is in
/// MICROSECONDS (unlike max_ack_delay, which is milliseconds).
pub const min_ack_delay_tp_id: u64 = 0xff04de1b;

pub const AckFrequencyFrame = struct {
/// Monotonically increasing per sender; receiver processes a frame only
/// when its sequence number is strictly greater than any previously
/// processed one (out-of-order/duplicate frames are ignored, not errors).
sequence_number: u64,
/// Max ack-eliciting packets the recipient may receive without sending an
/// ACK. 0 = acknowledge every ack-eliciting packet; 1 = RFC 9000 default
/// (every second packet).
ack_eliciting_threshold: u64,
/// Requested max_ack_delay in MICROSECONDS. MUST be >= the receiving
/// endpoint's advertised min_ack_delay or the connection errors with
/// PROTOCOL_VIOLATION (draft §4).
request_max_ack_delay_us: u64,
/// Out-of-order tolerance before an immediate ACK: 0 = reordering never
/// elicits an immediate ACK; 1 = any reordering does (RFC 9000 default).
reordering_threshold: u64,

pub const ParseResult = struct {
frame: AckFrequencyFrame,
/// Bytes consumed from `buf` (frame body only — the type varint has
/// already been consumed by the frame-loop dispatcher).
consumed: usize,
};

/// Parse the frame body (after the type varint).
pub fn parse(buf: []const u8) varint.DecodeError!ParseResult {
var pos: usize = 0;
const seq = try varint.decode(buf[pos..]);
pos += seq.len;
const thresh = try varint.decode(buf[pos..]);
pos += thresh.len;
const delay = try varint.decode(buf[pos..]);
pos += delay.len;
const reorder = try varint.decode(buf[pos..]);
pos += reorder.len;
return .{
.frame = .{
.sequence_number = seq.value,
.ack_eliciting_threshold = thresh.value,
.request_max_ack_delay_us = delay.value,
.reordering_threshold = reorder.value,
},
.consumed = pos,
};
}

/// Serialize including the leading type varint. Returns bytes written.
pub fn serialize(self: *const AckFrequencyFrame, buf: []u8) (varint.EncodeError || varint.DecodeError)!usize {
var pos: usize = 0;
const fields = [_]u64{
ack_frequency_frame_type,
self.sequence_number,
self.ack_eliciting_threshold,
self.request_max_ack_delay_us,
self.reordering_threshold,
};
for (fields) |v| {
const enc = try varint.encode(buf[pos..], v);
pos += enc.len;
}
return pos;
}
};

/// Serialize an IMMEDIATE_ACK frame (type only, no body).
pub fn serializeImmediateAck(buf: []u8) (varint.EncodeError || varint.DecodeError)!usize {
const enc = try varint.encode(buf, immediate_ack_frame_type);
return enc.len;
}

test "ack_frequency: serialize/parse round-trip" {
const testing = std.testing;
const f = AckFrequencyFrame{
.sequence_number = 7,
.ack_eliciting_threshold = 10,
.request_max_ack_delay_us = 25_000,
.reordering_threshold = 1,
};
var buf: [64]u8 = undefined;
const n = try f.serialize(&buf);
// Type 0xaf > 0x3f → 2-byte varint on the wire.
try testing.expectEqual(@as(u8, 0x40), buf[0]);
try testing.expectEqual(@as(u8, 0xaf), buf[1]);

const ft = try varint.decode(buf[0..n]);
try testing.expectEqual(ack_frequency_frame_type, ft.value);
const r = try AckFrequencyFrame.parse(buf[ft.len..n]);
try testing.expectEqual(f.sequence_number, r.frame.sequence_number);
try testing.expectEqual(f.ack_eliciting_threshold, r.frame.ack_eliciting_threshold);
try testing.expectEqual(f.request_max_ack_delay_us, r.frame.request_max_ack_delay_us);
try testing.expectEqual(f.reordering_threshold, r.frame.reordering_threshold);
try testing.expectEqual(n - ft.len, r.consumed);
}

test "ack_frequency: parse rejects truncated body" {
const testing = std.testing;
const f = AckFrequencyFrame{
.sequence_number = 300, // 2-byte varint — truncation lands mid-field
.ack_eliciting_threshold = 2,
.request_max_ack_delay_us = 1000,
.reordering_threshold = 0,
};
var buf: [64]u8 = undefined;
const n = try f.serialize(&buf);
const ft = try varint.decode(buf[0..n]);
var cut = ft.len;
while (cut < n) : (cut += 1) {
try testing.expectError(error.BufferTooShort, AckFrequencyFrame.parse(buf[ft.len..cut]));
}
}

test "ack_frequency: immediate ack is a single byte 0x1f" {
const testing = std.testing;
var buf: [8]u8 = undefined;
const n = try serializeImmediateAck(&buf);
try testing.expectEqual(@as(usize, 1), n);
try testing.expectEqual(@as(u8, 0x1f), buf[0]);
}

/// IMMEDIATE_ACK has no fields; empty struct for the frame registry union.
pub const ImmediateAck = struct {};
11 changes: 11 additions & 0 deletions src/frames/frame.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
//! PATH_RESPONSE 0x1b
//! CONNECTION_CLOSE 0x1c / 0x1d
//! HANDSHAKE_DONE 0x1e
//! IMMEDIATE_ACK 0x1f (draft-ietf-quic-ack-frequency)
//! DATAGRAM 0x30 / 0x31 (RFC 9221)
//! ACK_FREQUENCY 0xaf (draft-ietf-quic-ack-frequency)

const std = @import("std");
const varint = @import("../varint.zig");
Expand All @@ -32,6 +34,7 @@ const types = @import("../types.zig");
pub const ack = @import("ack.zig");
pub const crypto_frame = @import("crypto_frame.zig");
pub const datagram_mod = @import("datagram.zig");
pub const ack_frequency_mod = @import("ack_frequency.zig");
pub const stream = @import("stream.zig");
pub const transport = @import("transport.zig");

Expand Down Expand Up @@ -60,6 +63,7 @@ pub const FrameType = enum(u64) {
connection_close_quic = 0x1c,
connection_close_app = 0x1d,
handshake_done = 0x1e,
immediate_ack = 0x1f,
_,
};

Expand Down Expand Up @@ -88,6 +92,8 @@ pub const Frame = union(enum) {
connection_close: transport.ConnectionClose,
handshake_done: transport.HandshakeDone,
datagram: datagram_mod.DatagramFrame,
immediate_ack: ack_frequency_mod.ImmediateAck,
ack_frequency: ack_frequency_mod.AckFrequencyFrame,

/// Returns true if this frame type is allowed in Initial packets.
pub fn allowedInInitial(self: Frame) bool {
Expand Down Expand Up @@ -213,10 +219,15 @@ pub fn parseOne(buf: []const u8) ParseError!struct { frame: Frame, consumed: usi
return .{ .frame = .{ .connection_close = r.frame }, .consumed = pos + r.consumed };
},
0x1e => return .{ .frame = .{ .handshake_done = .{} }, .consumed = pos },
0x1f => return .{ .frame = .{ .immediate_ack = .{} }, .consumed = pos },
0x30, 0x31 => {
const r = try datagram_mod.DatagramFrame.parse(buf[pos..], ft);
return .{ .frame = .{ .datagram = r.frame }, .consumed = pos + r.consumed };
},
ack_frequency_mod.ack_frequency_frame_type => {
const r = try ack_frequency_mod.AckFrequencyFrame.parse(buf[pos..]);
return .{ .frame = .{ .ack_frequency = r.frame }, .consumed = pos + r.consumed };
},
else => return error.UnknownFrameType,
}
}
Expand Down
Loading
Loading