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 }