lib/quic/src/connection/params.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const quic = @import("../root.zig");
3
4 pub const Error = quic.transport.EncodeError || quic.transport.DecodeError ||
5 error{ InvalidConnectionId, InvalidRole, NoSpace };
6
7 pub fn encode(
8 config: quic.connection.Config,
9 limits: quic.connection.Limits,
10 local: quic.packet.ConnectionId,
11 original: ?quic.packet.ConnectionId,
12 bytes: []u8,
13 ) Error![]const u8 {
14 var output = quic.cursor.Write.init(bytes);
15 const receive_bytes: u62 = limits.stream_receive_bytes;
16 const stream_local = @min(config.initial_max_stream_data_bidi_local, receive_bytes);
17 const stream_remote = @min(config.initial_max_stream_data_bidi_remote, receive_bytes);
18 try quic.transport.encode(.{
19 .original_destination_connection_id = original,
20 .max_idle_timeout = config.max_idle_timeout,
21 .max_udp_payload_size = limits.datagram_bytes,
22 .initial_max_data = config.initial_max_data,
23 .initial_max_stream_data_bidi_local = stream_local,
24 .initial_max_stream_data_bidi_remote = stream_remote,
25 .initial_max_streams_bidi = localStreamsBidi(config.role),
26 .ack_delay_exponent = config.ack_delay_exponent,
27 .max_ack_delay = config.max_ack_delay,
28 .active_connection_id_limit = 2,
29 .initial_source_connection_id = local,
30 }, &output);
31 return output.written();
32 }
33
34 pub fn validatePeer(
35 role: quic.tls.Role,
36 bytes: []const u8,
37 peer_source: quic.packet.ConnectionId,
38 original: ?quic.packet.ConnectionId,
39 ) Error!quic.transport.Parameters {
40 const parameters = try quic.transport.decode(bytes);
41 const initial_source = parameters.initial_source_connection_id orelse
42 return error.InvalidConnectionId;
43 if (!equalCid(initial_source, peer_source)) return error.InvalidConnectionId;
44 if (parameters.retry_source_connection_id != null) return error.InvalidRole;
45 switch (role) {
46 .client => {
47 const expected = original orelse return error.InvalidConnectionId;
48 const received = parameters.original_destination_connection_id orelse
49 return error.InvalidConnectionId;
50 if (!equalCid(expected, received)) return error.InvalidConnectionId;
51 },
52 .server => if (parameters.original_destination_connection_id != null) {
53 return error.InvalidRole;
54 } else if (parameters.stateless_reset_token != null) {
55 return error.InvalidRole;
56 } else if (parameters.preferred_address != null) return error.InvalidRole,
57 }
58 return parameters;
59 }
60
61 /// Says how many bidirectional streams this endpoint lets the peer open, as the limit this endpoint
62 /// advertises and the same limit that arriving frames are held to. A server answers 1, which is
63 /// room for the client's stream 0, and a client answers 0. The figure travels to the peer among the
64 /// transport parameters, and `admitStream` closes with STREAM_LIMIT_ERROR for anything past it.
65 pub fn localStreamsBidi(role: quic.tls.Role) u62 {
66 return switch (role) {
67 .client => 0,
68 .server => 1,
69 };
70 }
71
72 /// Takes the lesser of the reassembly buffer and the limit configured for stream 0, sizing the
73 /// receiving half of stream 0 so the advertised window stays within the buffer behind it. Which
74 /// configured limit that is depends on the role, because the client is the side that opens
75 /// stream 0. `Connection.init` hands the result to the receiving half as its window.
76 pub fn streamReceiveWindow(config: quic.connection.Config, limits: quic.connection.Limits) u62 {
77 const advertised = switch (config.role) {
78 .client => config.initial_max_stream_data_bidi_local,
79 .server => config.initial_max_stream_data_bidi_remote,
80 };
81 return @min(advertised, @as(u62, limits.stream_receive_bytes));
82 }
83
84 /// Reads out of the peer's parameters how many stream 0 bytes this endpoint may send before the
85 /// peer grants more. A client reads the peer's remote-initiated figure and a server the peer's
86 /// local-initiated one, because the two name the same stream from opposite sides.
87 /// `authenticatePeer` lifts the sending half's credit to it as soon as the connection has checked
88 /// the peer's parameters.
89 pub fn streamSendLimit(role: quic.tls.Role, peer: quic.transport.Parameters) u62 {
90 return switch (role) {
91 .client => peer.initial_max_stream_data_bidi_remote,
92 .server => peer.initial_max_stream_data_bidi_local,
93 };
94 }
95
96 pub fn effectiveIdle(local: u62, peer: u62) u62 {
97 if (local == 0) return peer;
98 if (peer == 0) return local;
99 return @min(local, peer);
100 }
101
102 fn equalCid(left: quic.packet.ConnectionId, right: quic.packet.ConnectionId) bool {
103 return std.mem.eql(u8, left.slice(), right.slice());
104 }
105
106 test "RFC 9000 section 7.3 transport parameters authenticate connection IDs" {
107 const local = try quic.packet.ConnectionId.init("local-id");
108 const original = try quic.packet.ConnectionId.init("original");
109 const config = testConfig();
110 const limits = testLimits();
111 var bytes: [256]u8 = undefined;
112 const encoded = try encode(config, limits, local, original, &bytes);
113 const decoded = try validatePeer(.client, encoded, local, original);
114 try std.testing.expectEqual(@as(u62, 0), decoded.initial_max_streams_bidi);
115 }
116
117 test "RFC 9000 section 4.6 and section 18.2 stream parameters cap at the receive buffer" {
118 var config = testConfig();
119 config.role = .server;
120 config.initial_max_stream_data_bidi_remote = 5_000;
121 const limits = testLimits();
122 const local = try quic.packet.ConnectionId.init("server-id");
123 var bytes: [256]u8 = undefined;
124 const decoded = try quic.transport.decode(try encode(config, limits, local, null, &bytes));
125 try std.testing.expectEqual(@as(u62, 1), decoded.initial_max_streams_bidi);
126 try std.testing.expectEqual(@as(u62, 50), decoded.initial_max_stream_data_bidi_local);
127 try std.testing.expectEqual(@as(u62, 64), decoded.initial_max_stream_data_bidi_remote);
128 try std.testing.expectEqual(@as(u62, 64), streamReceiveWindow(config, limits));
129 try std.testing.expectEqual(@as(u62, 50), streamSendLimit(.server, decoded));
130 try std.testing.expectEqual(@as(u62, 64), streamSendLimit(.client, decoded));
131 }
132
133 test "RFC 9000 section 18.2 client stateless reset token is rejected" {
134 const peer = try quic.packet.ConnectionId.init("client-id");
135 var bytes: [256]u8 = undefined;
136 var output = quic.cursor.Write.init(&bytes);
137 try quic.transport.encode(.{
138 .stateless_reset_token = @splat(0x55),
139 .initial_source_connection_id = peer,
140 }, &output);
141 try std.testing.expectError(
142 error.InvalidRole,
143 validatePeer(.server, output.written(), peer, null),
144 );
145 }
146
147 test "RFC 9000 section 18.2 client preferred address is rejected" {
148 const peer = try quic.packet.ConnectionId.init("client-id");
149 const preferred = try quic.packet.ConnectionId.init("preferred");
150 var bytes: [256]u8 = undefined;
151 var output = quic.cursor.Write.init(&bytes);
152 try quic.transport.encode(.{
153 .preferred_address = .{
154 .ipv4 = @splat(0),
155 .ipv4_port = 0,
156 .ipv6 = @splat(0),
157 .ipv6_port = 0,
158 .connection_id = preferred,
159 .reset_token = @splat(0x66),
160 },
161 .initial_source_connection_id = peer,
162 }, &output);
163 try std.testing.expectError(
164 error.InvalidRole,
165 validatePeer(.server, output.written(), peer, null),
166 );
167 }
168
169 fn testLimits() quic.connection.Limits {
170 return .{
171 .tls_message_max = 256,
172 .crypto_buffer_bytes = 512,
173 .sent_records = 4,
174 .received_ranges = 4,
175 .datagram_bytes = 1200,
176 .stream_send_bytes = 64,
177 .stream_receive_bytes = 64,
178 .stream_receive_ranges = 4,
179 .stream_sent_ranges = 4,
180 };
181 }
182
183 fn testConfig() quic.connection.Config {
184 const identity = quic.tls.Identity.fromSeed(@splat(1)) catch unreachable;
185 return .{
186 .role = .client,
187 .identity = identity,
188 .expected_peer = null,
189 .alpn = "tiny/1",
190 .server_name = null,
191 .random = quic.tls.Random.testing(.{ .hello = @splat(2), .key_exchange = @splat(3) }),
192 .local_cid = "local-id",
193 .max_idle_timeout = 10,
194 .ack_delay_exponent = 3,
195 .max_ack_delay = 25,
196 .initial_max_data = 100,
197 .initial_max_stream_data_bidi_local = 50,
198 .initial_max_stream_data_bidi_remote = 50,
199 };
200 }