lib/quic/src/connection/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const alloc_phase = @import("alloc_phase");
2 const std = @import("std");
3 const quic = @import("../root.zig");
4
5 pub const key_slot_count: usize = 8;
6
7 pub const KeySlot = enum(u3) {
8 initial_write,
9 initial_read,
10 handshake_write,
11 handshake_read,
12 application_write,
13 application_read_previous,
14 application_read_current,
15 application_read_next,
16 };
17
18 const StorageLimits = quic.connection.Limits;
19
20 const CapacityDeriveError = error{
21 CapacityOverflow,
22 LimitsEmpty,
23 DatagramTooSmall,
24 DatagramTooLarge,
25 RangeLimitExceeded,
26 };
27
28 /// Capacity works out, from the limits alone, where each buffer sits in the block and how many
29 /// bytes the block needs in total. A caller derives one to learn how large a byte array to hand
30 /// `Storage.init`. Its `storage_bytes` field is the figure a caller sizes its array by. Derivation
31 /// fails with `LimitsEmpty`, `RangeLimitExceeded`, `DatagramTooSmall`, `DatagramTooLarge`, or
32 /// `CapacityOverflow`, which puts invalid limits out before any storage is touched. The layout
33 /// covers the sent records and received ranges of all three spaces, the TLS storage, the CRYPTO
34 /// lanes, the packet, scratch, and reason bytes, the eight key slots, the encoded transport
35 /// parameters, and the stream 0 buffers and tables.
36 pub const Capacity = struct {
37 sent_offset: usize,
38 sent_count: usize,
39 sent_bytes: usize,
40 ranges_offset: usize,
41 range_count: usize,
42 range_bytes: usize,
43 tls_offset: usize,
44 tls_bytes: usize,
45 crypto_offset: usize,
46 crypto_lane_bytes: usize,
47 packet_offset: usize,
48 scratch_offset: usize,
49 reason_offset: usize,
50 keys_offset: usize,
51 keys_bytes: usize,
52 transport_offset: usize,
53 transport_bytes: usize,
54 stream_sent_offset: usize,
55 stream_range_offset: usize,
56 stream_send_offset: usize,
57 stream_receive_offset: usize,
58 storage_bytes: usize,
59
60 pub const DeriveError: type = CapacityDeriveError;
61
62 pub fn derive(limits: StorageLimits) DeriveError!Capacity {
63 try validateLimits(limits);
64 const sent_count = try multiplied(limits.sent_records, 3);
65 const sent = try placed(quic.connection.SentRecord, 0, sent_count);
66 const range_count = try multiplied(limits.received_ranges, 3);
67 const ranges = try placed(quic.connection.Range, sent.end, range_count);
68 const tls_offset = try aligned(ranges.end, quic.tls.Storage.storage_alignment);
69 const tls_capacity = quic.tls.Capacity.derive(.{
70 .max_message = limits.tls_message_max,
71 }) catch return error.CapacityOverflow;
72 const crypto_offset = try added(tls_offset, tls_capacity.storage_bytes);
73 const crypto_lane_bytes: usize = limits.crypto_buffer_bytes;
74 const crypto_bytes = try multiplied(crypto_lane_bytes, 6);
75 const packet_offset = try added(crypto_offset, crypto_bytes);
76 const scratch_offset = try added(packet_offset, limits.datagram_bytes);
77 const reason_offset = try added(scratch_offset, limits.datagram_bytes);
78 const reason_end = try added(reason_offset, limits.datagram_bytes);
79 const keys_offset = try aligned(reason_end, quic.crypto.Keys.storage_alignment);
80 const keys_bytes = try multiplied(key_slot_count, quic.crypto.Keys.storage_bytes_max);
81 const transport_offset = try added(keys_offset, keys_bytes);
82 const transport_end = try added(transport_offset, limits.tls_message_max);
83 const streams = try placeStreams(limits, transport_end);
84 return .{
85 .sent_offset = sent.start,
86 .sent_count = sent_count,
87 .sent_bytes = sent.bytes,
88 .ranges_offset = ranges.start,
89 .range_count = range_count,
90 .range_bytes = ranges.bytes,
91 .tls_offset = tls_offset,
92 .tls_bytes = tls_capacity.storage_bytes,
93 .crypto_offset = crypto_offset,
94 .crypto_lane_bytes = crypto_lane_bytes,
95 .packet_offset = packet_offset,
96 .scratch_offset = scratch_offset,
97 .reason_offset = reason_offset,
98 .keys_offset = keys_offset,
99 .keys_bytes = keys_bytes,
100 .transport_offset = transport_offset,
101 .transport_bytes = limits.tls_message_max,
102 .stream_sent_offset = streams.sent.start,
103 .stream_range_offset = streams.ranges.start,
104 .stream_send_offset = streams.send.start,
105 .stream_receive_offset = streams.receive.start,
106 .storage_bytes = streams.receive.end,
107 };
108 }
109 };
110
111 const StorageCapacity = Capacity;
112
113 const Region = struct { start: usize, bytes: usize, end: usize };
114
115 const StreamRegions = struct { sent: Region, ranges: Region, send: Region, receive: Region };
116
117 fn validateLimits(limits: StorageLimits) Capacity.DeriveError!void {
118 if (limits.tls_message_max == 0) return error.LimitsEmpty;
119 if (limits.crypto_buffer_bytes == 0) return error.LimitsEmpty;
120 if (limits.sent_records == 0) return error.LimitsEmpty;
121 if (limits.received_ranges == 0) return error.LimitsEmpty;
122 if (limits.stream_send_bytes == 0) return error.LimitsEmpty;
123 if (limits.stream_receive_bytes == 0) return error.LimitsEmpty;
124 if (limits.stream_receive_ranges == 0) return error.LimitsEmpty;
125 if (limits.stream_sent_ranges == 0) return error.LimitsEmpty;
126 if (limits.received_ranges > quic.frame.ack_ranges_max) {
127 return error.RangeLimitExceeded;
128 }
129 if (limits.datagram_bytes < 1200) return error.DatagramTooSmall;
130 if (limits.datagram_bytes > quic.crypto.packet.packet_bytes_max) {
131 return error.DatagramTooLarge;
132 }
133 }
134
135 fn added(left: anytype, right: anytype) Capacity.DeriveError!usize {
136 const bounded_left: usize = @intCast(left);
137 const bounded_right: usize = @intCast(right);
138 return std.math.add(usize, bounded_left, bounded_right) catch error.CapacityOverflow;
139 }
140
141 fn multiplied(left: anytype, right: anytype) Capacity.DeriveError!usize {
142 const bounded_left: usize = @intCast(left);
143 const bounded_right: usize = @intCast(right);
144 return std.math.mul(usize, bounded_left, bounded_right) catch error.CapacityOverflow;
145 }
146
147 fn aligned(value: usize, alignment: usize) Capacity.DeriveError!usize {
148 const mask = alignment - 1;
149 return (try added(value, mask)) & ~mask;
150 }
151
152 fn placed(comptime T: type, offset: usize, count: usize) Capacity.DeriveError!Region {
153 const start = try aligned(offset, @alignOf(T));
154 const bytes = try multiplied(count, @sizeOf(T));
155 return .{ .start = start, .bytes = bytes, .end = try added(start, bytes) };
156 }
157
158 fn placeStreams(limits: StorageLimits, offset: usize) Capacity.DeriveError!StreamRegions {
159 const sent = try placed(quic.connection.stream.SentRange, offset, limits.stream_sent_ranges);
160 const ranges = try placed(quic.connection.stream.Range, sent.end, limits.stream_receive_ranges);
161 const send = try placed(u8, ranges.end, limits.stream_send_bytes);
162 const receive = try placed(u8, send.end, limits.stream_receive_bytes);
163 return .{ .sent = sent, .ranges = ranges, .send = send, .receive = receive };
164 }
165
166 /// Storage cuts the caller's block into every typed buffer one connection works out of. A caller
167 /// sets one up over its own bytes and hands a pointer to `Connection.init`, which keeps that
168 /// pointer. The block and the connection both have to stay put for the connection's life, because
169 /// the connection and the key state each hold a pointer to it. A block shorter than the derived
170 /// total is refused with `StorageTooShort`, and nothing else happens. `activate` moves it from
171 /// setup into steady use, and `Connection.init` does that once the connection stands. `deinit`
172 /// wipes every byte and hands the block back, and it requires that the connection has already
173 /// released the TLS storage nested inside. `records`, `ranges`, and `keys` hand out the per-space
174 /// and per-slot views of the block.
175 pub const Storage = struct {
176 phase: alloc_phase.capacity.Phase,
177 capacity: StorageCapacity,
178 storage: []align(quic.tls.Storage.storage_alignment) u8,
179 sent_records: []quic.connection.SentRecord,
180 received_ranges: []quic.connection.Range,
181 tls_storage: quic.tls.Storage,
182 crypto_bytes: [3][]u8,
183 crypto_present: [3][]u8,
184 packet: []u8,
185 scratch: []u8,
186 reason: []u8,
187 key_bytes: []u8,
188 transport: []u8,
189 stream_sent_ranges: []quic.connection.stream.SentRange,
190 stream_receive_ranges: []quic.connection.stream.Range,
191 stream_send_bytes: []u8,
192 stream_receive_bytes: []u8,
193 nested_released: bool = false,
194
195 pub const storage_alignment: usize = quic.tls.Storage.storage_alignment;
196 pub const Storage = []align(storage_alignment) u8;
197 pub const Limits: type = StorageLimits;
198 pub const Capacity: type = StorageCapacity;
199 pub const InitError = StorageCapacity.DeriveError || quic.tls.Storage.InitError ||
200 error{StorageTooShort};
201 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
202 .transition_steps_max = 1,
203 .cleanup_steps_per_call_max = 0,
204 .cleanup_calls_at_capacity_max = 0,
205 };
206
207 pub const claim: alloc_phase.capacity.Declaration = .{
208 .source = .{
209 .id = "quic.connection_storage",
210 .kind = .startup_static,
211 .limit_source = .caller,
212 .storage = .{
213 .covered = &.{
214 .{
215 .id = "packet_spaces",
216 .lifetime = .steady,
217 .detail = "three bounded sent-record and received-range lanes",
218 },
219 .{
220 .id = "crypto_and_tls",
221 .lifetime = .steady,
222 .detail = "TLS storage, CRYPTO reassembly, and packet protection keys",
223 },
224 .{
225 .id = "datagram_work",
226 .lifetime = .steady,
227 .detail = "packet input, open scratch, and close reason bytes",
228 },
229 .{
230 .id = "stream_buffers",
231 .lifetime = .steady,
232 .detail = "stream 0 send and receive rings with sent and received ranges",
233 },
234 },
235 .excluded = &.{
236 "caller configuration slices and identity key storage",
237 "caller datagram input and output slices",
238 "standard cryptographic primitive stack storage",
239 },
240 },
241 .capacity = .{
242 .inputs = &.{
243 alloc_phase.capacity.bindInput(
244 StorageLimits,
245 "tls_message_max",
246 "tls_message_max",
247 ),
248 alloc_phase.capacity.bindInput(
249 StorageLimits,
250 "crypto_buffer_bytes",
251 "crypto_buffer_bytes",
252 ),
253 alloc_phase.capacity.bindInput(
254 StorageLimits,
255 "sent_records",
256 "sent_records",
257 ),
258 alloc_phase.capacity.bindInput(
259 StorageLimits,
260 "received_ranges",
261 "received_ranges",
262 ),
263 alloc_phase.capacity.bindInput(
264 StorageLimits,
265 "datagram_bytes",
266 "datagram_bytes",
267 ),
268 alloc_phase.capacity.bindInput(
269 StorageLimits,
270 "stream_send_bytes",
271 "stream_send_bytes",
272 ),
273 alloc_phase.capacity.bindInput(
274 StorageLimits,
275 "stream_receive_bytes",
276 "stream_receive_bytes",
277 ),
278 alloc_phase.capacity.bindInput(
279 StorageLimits,
280 "stream_receive_ranges",
281 "stream_receive_ranges",
282 ),
283 alloc_phase.capacity.bindInput(
284 StorageLimits,
285 "stream_sent_ranges",
286 "stream_sent_ranges",
287 ),
288 },
289 .type_selectors = &.{
290 alloc_phase.capacity.bindType(
291 quic.connection.SentRecord,
292 "sent_record",
293 ),
294 alloc_phase.capacity.bindType(
295 quic.connection.Range,
296 "received_range",
297 ),
298 alloc_phase.capacity.bindType(
299 quic.connection.stream.SentRange,
300 "stream_sent_range",
301 ),
302 alloc_phase.capacity.bindType(
303 quic.connection.stream.Range,
304 "stream_received_range",
305 ),
306 },
307 .nodes = &.{
308 .{ .input = 2 },
309 .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 3 } } },
310 .{ .scale = .{
311 .node = 1,
312 .coefficient = .{ .size_of_concrete_type = 0 },
313 } },
314 .{ .alignment = .{
315 .node = 2,
316 .alignment = .{ .concrete_type = 1 },
317 } },
318 .{ .input = 3 },
319 .{ .scale = .{ .node = 4, .coefficient = .{ .literal = 3 } } },
320 .{ .scale = .{
321 .node = 5,
322 .coefficient = .{ .size_of_concrete_type = 1 },
323 } },
324 .{ .add = .{ .left = 3, .right = 6 } },
325 .{ .alignment = .{ .node = 7, .alignment = .{ .literal = 16 } } },
326 .{ .input = 0 },
327 .{ .scale = .{ .node = 9, .coefficient = .{ .literal = 8 } } },
328 .{ .constant = 28 },
329 .{ .add = .{ .left = 10, .right = 11 } },
330 .{ .add = .{ .left = 8, .right = 12 } },
331 .{ .input = 1 },
332 .{ .scale = .{ .node = 14, .coefficient = .{ .literal = 6 } } },
333 .{ .add = .{ .left = 13, .right = 15 } },
334 .{ .input = 4 },
335 .{ .scale = .{ .node = 17, .coefficient = .{ .literal = 3 } } },
336 .{ .add = .{ .left = 16, .right = 18 } },
337 .{ .constant = 608 },
338 .{ .add = .{ .left = 19, .right = 20 } },
339 .{ .add = .{ .left = 21, .right = 9 } },
340 .{ .alignment = .{
341 .node = 22,
342 .alignment = .{ .concrete_type = 2 },
343 } },
344 .{ .input = 8 },
345 .{ .scale = .{
346 .node = 24,
347 .coefficient = .{ .size_of_concrete_type = 2 },
348 } },
349 .{ .add = .{ .left = 23, .right = 25 } },
350 .{ .alignment = .{
351 .node = 26,
352 .alignment = .{ .concrete_type = 3 },
353 } },
354 .{ .input = 7 },
355 .{ .scale = .{
356 .node = 28,
357 .coefficient = .{ .size_of_concrete_type = 3 },
358 } },
359 .{ .add = .{ .left = 27, .right = 29 } },
360 .{ .input = 5 },
361 .{ .add = .{ .left = 30, .right = 31 } },
362 .{ .input = 6 },
363 .{ .add = .{ .left = 32, .right = 33 } },
364 },
365 .assertions = &.{.{
366 .scope = .closure_total,
367 .measure = .retained,
368 .relation = .exact,
369 .expression = 34,
370 }},
371 },
372 .overload = .{
373 .kind = .reject_before_seal,
374 .detail = "invalid limits and short caller storage reject before activation",
375 },
376 .risks = .{
377 .transitive = .{
378 .status = .witnessed,
379 .detail = "packet, frame, TLS, and crypto paths retain only provisioned slices",
380 },
381 .foreign = .{
382 .status = .excluded,
383 .detail = "the sans-I/O connection crosses no socket or foreign boundary",
384 },
385 },
386 .work = .{
387 .equation = "each operation scans caller limits or one datagram at most",
388 },
389 .obligations = &.{
390 .{ .key = "quic_connection_capacity", .role = .capacity_model },
391 .{ .key = "quic_connection_overload", .role = .overload },
392 .{ .key = "quic_connection_transitive", .role = .transitive_risk },
393 .{ .key = "quic_connection_work", .role = .work_bound },
394 .{ .key = "quic_connection_root", .role = .custom },
395 },
396 },
397 .bindings = .{
398 .owner = @This(),
399 .seal = .{
400 .family = alloc_phase.capacity.selector(@This().activate),
401 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
402 },
403 .teardown = .{
404 .family = alloc_phase.capacity.selector(@This().deinit),
405 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
406 },
407 },
408 };
409
410 pub fn init(bytes: @This().Storage, limits: StorageLimits) InitError!@This() {
411 const capacity = try StorageCapacity.derive(limits);
412 if (bytes.len < capacity.storage_bytes) return error.StorageTooShort;
413 const owned = bytes[0..capacity.storage_bytes];
414 const sent_slice = typedSlice(
415 quic.connection.SentRecord,
416 owned,
417 capacity.sent_offset,
418 capacity.sent_count,
419 );
420 const range_slice = typedSlice(
421 quic.connection.Range,
422 owned,
423 capacity.ranges_offset,
424 capacity.range_count,
425 );
426 const stream_sent_slice = typedSlice(
427 quic.connection.stream.SentRange,
428 owned,
429 capacity.stream_sent_offset,
430 limits.stream_sent_ranges,
431 );
432 const stream_range_slice = typedSlice(
433 quic.connection.stream.Range,
434 owned,
435 capacity.stream_range_offset,
436 limits.stream_receive_ranges,
437 );
438 const send_start = capacity.stream_send_offset;
439 const receive_start = capacity.stream_receive_offset;
440 const tls_bytes: []align(quic.tls.Storage.storage_alignment) u8 = @alignCast(
441 owned[capacity.tls_offset..][0..capacity.tls_bytes],
442 );
443 var result = @This(){
444 .phase = .initialization,
445 .capacity = capacity,
446 .storage = owned,
447 .sent_records = sent_slice,
448 .received_ranges = range_slice,
449 .tls_storage = try quic.tls.Storage.init(tls_bytes, .{
450 .max_message = limits.tls_message_max,
451 }),
452 .crypto_bytes = undefined,
453 .crypto_present = undefined,
454 .packet = owned[capacity.packet_offset..][0..limits.datagram_bytes],
455 .scratch = owned[capacity.scratch_offset..][0..limits.datagram_bytes],
456 .reason = owned[capacity.reason_offset..][0..limits.datagram_bytes],
457 .key_bytes = owned[capacity.keys_offset..][0..capacity.keys_bytes],
458 .transport = owned[capacity.transport_offset..][0..capacity.transport_bytes],
459 .stream_sent_ranges = stream_sent_slice,
460 .stream_receive_ranges = stream_range_slice,
461 .stream_send_bytes = owned[send_start..][0..limits.stream_send_bytes],
462 .stream_receive_bytes = owned[receive_start..][0..limits.stream_receive_bytes],
463 };
464 result.partitionCrypto();
465 return result;
466 }
467
468 pub fn activate(self: *@This()) void {
469 std.debug.assert(self.phase == .initialization);
470 self.phase = .steady;
471 }
472
473 pub fn deinit(self: *@This()) @This().Storage {
474 std.debug.assert(self.phase == .steady);
475 std.debug.assert(self.nested_released);
476 std.crypto.secureZero(u8, self.storage);
477 self.phase = .teardown;
478 const bytes = self.storage;
479 self.* = undefined;
480 return bytes;
481 }
482
483 pub fn records(self: *@This(), index: usize) []quic.connection.SentRecord {
484 std.debug.assert(index < 3);
485 const width = self.capacity.sent_count / 3;
486 return self.sent_records[index * width ..][0..width];
487 }
488
489 pub fn ranges(self: *@This(), index: usize) []quic.connection.Range {
490 std.debug.assert(index < 3);
491 const width = self.capacity.range_count / 3;
492 return self.received_ranges[index * width ..][0..width];
493 }
494
495 pub fn keys(self: *@This(), slot: KeySlot) quic.crypto.Keys.Storage {
496 const width = quic.crypto.Keys.storage_bytes_max;
497 const start = @as(usize, @backingInt(slot)) * width;
498 return @alignCast(self.key_bytes[start..][0..width]);
499 }
500
501 fn partitionCrypto(self: *@This()) void {
502 const width = self.capacity.crypto_lane_bytes;
503 for (0..3) |index| {
504 const data_start = self.capacity.crypto_offset + index * width;
505 const present_start = self.capacity.crypto_offset + (3 + index) * width;
506 self.crypto_bytes[index] = self.storage[data_start..][0..width];
507 self.crypto_present[index] = self.storage[present_start..][0..width];
508 }
509 }
510 };
511
512 fn typedSlice(
513 comptime T: type,
514 bytes: []u8,
515 offset: usize,
516 count: usize,
517 ) []T {
518 const byte_count = count * @sizeOf(T);
519 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
520 return std.mem.bytesAsSlice(T, region);
521 }
522
523 comptime {
524 std.debug.assert(key_slot_count * quic.crypto.Keys.storage_bytes_max == 608);
525 }
526
527 comptime {
528 alloc_phase.capacity.requireProvisionedExactOwnerShape(Storage);
529 }
530
531 test "connection storage accepts exact capacity and rejects one byte less" {
532 comptime {
533 @stardustClaim(
534 alloc_phase.capacity.witness(Storage, "quic_connection_capacity"),
535 null,
536 null,
537 null,
538 null,
539 null,
540 null,
541 );
542 @stardustClaim(
543 alloc_phase.capacity.witness(Storage, "quic_connection_overload"),
544 null,
545 null,
546 null,
547 null,
548 null,
549 null,
550 );
551 @stardustClaim(
552 alloc_phase.capacity.witness(Storage, "quic_connection_transitive"),
553 null,
554 null,
555 null,
556 null,
557 null,
558 null,
559 );
560 @stardustClaim(
561 alloc_phase.capacity.witness(Storage, "quic_connection_work"),
562 null,
563 null,
564 null,
565 null,
566 null,
567 null,
568 );
569 @stardustClaim(
570 alloc_phase.capacity.witness(Storage, "quic_connection_root"),
571 null,
572 null,
573 null,
574 null,
575 null,
576 null,
577 );
578 }
579 const limits = comptime testLimits();
580 const capacity = comptime Capacity.derive(limits) catch unreachable;
581 var bytes: [capacity.storage_bytes]u8 align(Storage.storage_alignment) = undefined;
582 try std.testing.expectError(
583 error.StorageTooShort,
584 Storage.init(@alignCast(bytes[0 .. bytes.len - 1]), limits),
585 );
586 var storage = try Storage.init(&bytes, limits);
587 storage.tls_storage.activate();
588 _ = storage.tls_storage.deinit();
589 storage.nested_released = true;
590 storage.activate();
591 _ = storage.deinit();
592 }
593
594 test "RFC 9000 section 19.3 received ACK range capacity maximum and maximum plus one" {
595 var limits = testLimits();
596 limits.received_ranges = quic.frame.ack_ranges_max;
597 _ = try Capacity.derive(limits);
598 limits.received_ranges += 1;
599 try std.testing.expectError(error.RangeLimitExceeded, Capacity.derive(limits));
600 }
601
602 test "RFC 9000 section 18.2 datagram capacity maximum and maximum plus one" {
603 var limits = testLimits();
604 limits.datagram_bytes = 65_527;
605 _ = try Capacity.derive(limits);
606 limits.datagram_bytes += 1;
607 try std.testing.expectError(error.DatagramTooLarge, Capacity.derive(limits));
608 }
609
610 test "connection storage rejects empty stream limits" {
611 const base = testLimits();
612 _ = try Capacity.derive(base);
613 var limits = base;
614 limits.stream_send_bytes = 0;
615 try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
616 limits = base;
617 limits.stream_receive_bytes = 0;
618 try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
619 limits = base;
620 limits.stream_receive_ranges = 0;
621 try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
622 limits = base;
623 limits.stream_sent_ranges = 0;
624 try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
625 }
626
627 fn testLimits() StorageLimits {
628 return .{
629 .tls_message_max = 256,
630 .crypto_buffer_bytes = 512,
631 .sent_records = 4,
632 .received_ranges = 4,
633 .datagram_bytes = 1200,
634 .stream_send_bytes = 64,
635 .stream_receive_bytes = 64,
636 .stream_receive_ranges = 4,
637 .stream_sent_ranges = 4,
638 };
639 }