34a3c584
refactor: one owner for the client half of the QUIC handshake
a73x 2026-08-29 01:35
Commit message
src/client/quic_client.zig
| Old | New | ||
|---|---|---|---|
| @@ -52,11 +52,7 @@ fn pskClientCb( | |||
| 52 | ciphersuite: [*c][*c]const u8, | 52 | ciphersuite: [*c][*c]const u8, |
| 53 | ) callconv(.c) c_uint { | 53 | ) callconv(.c) c_uint { |
| 54 | const k = g_key orelse return 0; | 54 | const k = g_key orelse return 0; |
| 55 | if (id_max < 4 or key_max < key_len) return 0; | 55 | return quic.answerPsk(k, identity, id_max, key_out, key_max, ciphersuite); |
| 56 | @memcpy(identity[0..4], "mux\x00"); | ||
| 57 | @memcpy(key_out[0..key_len], &k.bytes); | ||
| 58 | if (ciphersuite) |cs| cs.* = quic.psk_ciphersuite; | ||
| 59 | return key_len; | ||
| 60 | } | 56 | } |
| 61 | 57 | ||
| 62 | fn getConnCb(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn { | 58 | fn getConnCb(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn { |
| @@ -197,23 +193,11 @@ pub const Client = struct { | |||
| 197 | } | 193 | } |
| 198 | 194 | ||
| 199 | fn startTls(self: *Client) !void { | 195 | fn startTls(self: *Client) !void { |
| 200 | if (c.wolfSSL_Init() != c.WOLFSSL_SUCCESS) return error.TlsInit; | 196 | // Set before clientTls, which hands wolfSSL a pointer to it. |
| 201 | const ctx = c.wolfSSL_CTX_new(c.wolfTLSv1_3_client_method()) orelse return error.TlsInit; | ||
| 202 | self.ssl_ctx = ctx; | ||
| 203 | if (c.ngtcp2_crypto_wolfssl_configure_client_context(ctx) != 0) return error.TlsInit; | ||
| 204 | c.wolfSSL_CTX_set_psk_client_tls13_callback(ctx, pskClientCb); | ||
| 205 | _ = c.wolfSSL_CTX_set_cipher_list(ctx, quic.psk_ciphersuite); | ||
| 206 | |||
| 207 | const ssl = c.wolfSSL_new(ctx) orelse return error.TlsInit; | ||
| 208 | self.ssl = ssl; | ||
| 209 | self.conn_ref = .{ .get_conn = getConnCb, .user_data = self }; | 197 | self.conn_ref = .{ .get_conn = getConnCb, .user_data = self }; |
| 210 | _ = c.wolfSSL_set_app_data(ssl, &self.conn_ref); | 198 | const tls = try quic.clientTls(pskClientCb, &self.conn_ref); |
| 211 | _ = c.wolfSSL_UseALPN( | 199 | self.ssl_ctx = tls.ctx; |
| 212 | ssl, | 200 | self.ssl = tls.ssl; |
| 213 | @constCast(quic.alpn[1..].ptr), | ||
| 214 | quic.alpn.len - 1, | ||
| 215 | c.WOLFSSL_ALPN_FAILED_ON_MISMATCH, | ||
| 216 | ); | ||
| 217 | } | 201 | } |
| 218 | 202 | ||
| 219 | fn startConn(self: *Client, idle_ms: u32) !void { | 203 | fn startConn(self: *Client, idle_ms: u32) !void { |
| @@ -224,22 +208,7 @@ pub const Client = struct { | |||
| 224 | scid.datalen = 8; | 208 | scid.datalen = 8; |
| 225 | std.crypto.random.bytes(scid.data[0..8]); | 209 | std.crypto.random.bytes(scid.data[0..8]); |
| 226 | 210 | ||
| 227 | var cbs: c.ngtcp2_callbacks = std.mem.zeroes(c.ngtcp2_callbacks); | 211 | var cbs = quic.clientCallbacks(); |
| 228 | cbs.client_initial = c.ngtcp2_crypto_client_initial_cb; | ||
| 229 | cbs.recv_crypto_data = c.ngtcp2_crypto_recv_crypto_data_cb; | ||
| 230 | cbs.encrypt = c.ngtcp2_crypto_encrypt_cb; | ||
| 231 | cbs.decrypt = c.ngtcp2_crypto_decrypt_cb; | ||
| 232 | cbs.hp_mask = c.ngtcp2_crypto_hp_mask_cb; | ||
| 233 | // The listener answers every fresh Initial with a Retry, so a client | ||
| 234 | // that cannot process one never gets past its first flight. | ||
| 235 | cbs.recv_retry = c.ngtcp2_crypto_recv_retry_cb; | ||
| 236 | cbs.update_key = c.ngtcp2_crypto_update_key_cb; | ||
| 237 | cbs.delete_crypto_aead_ctx = c.ngtcp2_crypto_delete_crypto_aead_ctx_cb; | ||
| 238 | cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb; | ||
| 239 | cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb; | ||
| 240 | cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb; | ||
| 241 | cbs.rand = quic.randCb; | ||
| 242 | cbs.get_new_connection_id2 = quic.getNewCidCb; | ||
| 243 | cbs.handshake_completed = handshakeCompletedCb; | 212 | cbs.handshake_completed = handshakeCompletedCb; |
| 244 | cbs.extend_max_local_streams_bidi = extendStreamsCb; | 213 | cbs.extend_max_local_streams_bidi = extendStreamsCb; |
| 245 | cbs.recv_stream_data = recvStreamDataCb; | 214 | cbs.recv_stream_data = recvStreamDataCb; |
| @@ -249,12 +218,7 @@ pub const Client = struct { | |||
| 249 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); | 218 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); |
| 250 | settings.initial_ts = quic.timestampNs(); | 219 | settings.initial_ts = quic.timestampNs(); |
| 251 | 220 | ||
| 252 | var params: c.ngtcp2_transport_params = undefined; | 221 | var params = quic.clientParams(); |
| 253 | c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, ¶ms); | ||
| 254 | params.initial_max_streams_bidi = 4; | ||
| 255 | params.initial_max_stream_data_bidi_local = 256 * 1024; | ||
| 256 | params.initial_max_stream_data_bidi_remote = 256 * 1024; | ||
| 257 | params.initial_max_data = 1024 * 1024; | ||
| 258 | // Tunable for the same reason as the daemon's: the reconnect loop | 222 | // Tunable for the same reason as the daemon's: the reconnect loop |
| 259 | // has to see a dead transport on a schedule a test can wait for. | 223 | // has to see a dead transport on a schedule a test can wait for. |
| 260 | params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000; | 224 | params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000; |
src/quic.zig
| Old | New | ||
|---|---|---|---|
| @@ -651,6 +651,95 @@ pub fn getNewCidCb( | |||
| 651 | } | 651 | } |
| 652 | 652 | ||
| 653 | // --------------------------------------------------------------------------- | 653 | // --------------------------------------------------------------------------- |
| 654 | // The client side of the handshake | ||
| 655 | // | ||
| 656 | // Two peers dial this daemon: the client transport in quic_client.zig and the | ||
| 657 | // test peer in quic_server.zig, which cannot import it (folder rule 2). They | ||
| 658 | // spelled the whole PSK/ngtcp2 preamble twice, and a drift in either fails the | ||
| 659 | // handshake with nothing to read but a TLS alert. What each of them chooses | ||
| 660 | // for itself — its key, its stream handlers, its idle timeout — it still sets | ||
| 661 | // on what these hand back. | ||
| 662 | // --------------------------------------------------------------------------- | ||
| 663 | |||
| 664 | /// Identity "mux", the key's bytes, our one ciphersuite: none is a choice. | ||
| 665 | pub fn answerPsk( | ||
| 666 | key: Key, | ||
| 667 | identity: [*c]u8, | ||
| 668 | id_max: c_uint, | ||
| 669 | key_out: [*c]u8, | ||
| 670 | key_max: c_uint, | ||
| 671 | ciphersuite: [*c][*c]const u8, | ||
| 672 | ) c_uint { | ||
| 673 | if (id_max < 4 or key_max < key_len) return 0; | ||
| 674 | @memcpy(identity[0..4], "mux\x00"); | ||
| 675 | @memcpy(key_out[0..key_len], &key.bytes); | ||
| 676 | if (ciphersuite) |cs| cs.* = psk_ciphersuite; | ||
| 677 | return key_len; | ||
| 678 | } | ||
| 679 | |||
| 680 | /// PSK over TLS 1.3, our ciphersuite, our ALPN. | ||
| 681 | pub fn clientTls( | ||
| 682 | psk_cb: c.wc_psk_client_tls13_callback, | ||
| 683 | conn_ref: *c.ngtcp2_crypto_conn_ref, | ||
| 684 | ) !struct { ctx: *c.WOLFSSL_CTX, ssl: *c.WOLFSSL } { | ||
| 685 | if (c.wolfSSL_Init() != c.WOLFSSL_SUCCESS) return error.TlsInit; | ||
| 686 | const ctx = c.wolfSSL_CTX_new(c.wolfTLSv1_3_client_method()) orelse return error.TlsInit; | ||
| 687 | // The caller records `ctx` only on success, so every failure past here | ||
| 688 | // owns the free. Both callers used to set their field first and lean on | ||
| 689 | // deinit for it; a shared owner cannot, having no field to set. | ||
| 690 | errdefer c.wolfSSL_CTX_free(ctx); | ||
| 691 | if (c.ngtcp2_crypto_wolfssl_configure_client_context(ctx) != 0) return error.TlsInit; | ||
| 692 | c.wolfSSL_CTX_set_psk_client_tls13_callback(ctx, psk_cb); | ||
| 693 | _ = c.wolfSSL_CTX_set_cipher_list(ctx, psk_ciphersuite); | ||
| 694 | const ssl = c.wolfSSL_new(ctx) orelse return error.TlsInit; | ||
| 695 | _ = c.wolfSSL_set_app_data(ssl, conn_ref); | ||
| 696 | _ = c.wolfSSL_UseALPN( | ||
| 697 | ssl, | ||
| 698 | @constCast(alpn[1..].ptr), | ||
| 699 | alpn.len - 1, | ||
| 700 | c.WOLFSSL_ALPN_FAILED_ON_MISMATCH, | ||
| 701 | ); | ||
| 702 | return .{ .ctx = ctx, .ssl = ssl }; | ||
| 703 | } | ||
| 704 | |||
| 705 | /// Every ngtcp2 callback a client side must install that is the library's | ||
| 706 | /// own rather than the caller's. The caller then sets what IS its own — | ||
| 707 | /// handshake completion, stream events — on the returned struct. | ||
| 708 | pub fn clientCallbacks() c.ngtcp2_callbacks { | ||
| 709 | var cbs: c.ngtcp2_callbacks = std.mem.zeroes(c.ngtcp2_callbacks); | ||
| 710 | cbs.client_initial = c.ngtcp2_crypto_client_initial_cb; | ||
| 711 | cbs.recv_crypto_data = c.ngtcp2_crypto_recv_crypto_data_cb; | ||
| 712 | cbs.encrypt = c.ngtcp2_crypto_encrypt_cb; | ||
| 713 | cbs.decrypt = c.ngtcp2_crypto_decrypt_cb; | ||
| 714 | cbs.hp_mask = c.ngtcp2_crypto_hp_mask_cb; | ||
| 715 | // The listener answers every fresh Initial with a Retry, so a client | ||
| 716 | // that cannot process one never gets past its first flight. | ||
| 717 | cbs.recv_retry = c.ngtcp2_crypto_recv_retry_cb; | ||
| 718 | cbs.update_key = c.ngtcp2_crypto_update_key_cb; | ||
| 719 | cbs.delete_crypto_aead_ctx = c.ngtcp2_crypto_delete_crypto_aead_ctx_cb; | ||
| 720 | cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb; | ||
| 721 | cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb; | ||
| 722 | cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb; | ||
| 723 | cbs.rand = randCb; | ||
| 724 | cbs.get_new_connection_id2 = getNewCidCb; | ||
| 725 | return cbs; | ||
| 726 | } | ||
| 727 | |||
| 728 | /// The flow-control window a mux client opens with. `max_idle_timeout` is | ||
| 729 | /// deliberately left at ngtcp2's default: only the reconnecting transport | ||
| 730 | /// tunes it, and a fixture that has no reconnect loop must not inherit a | ||
| 731 | /// number chosen for one. | ||
| 732 | pub fn clientParams() c.ngtcp2_transport_params { | ||
| 733 | var params: c.ngtcp2_transport_params = undefined; | ||
| 734 | c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, ¶ms); | ||
| 735 | params.initial_max_streams_bidi = 4; | ||
| 736 | params.initial_max_stream_data_bidi_local = 256 * 1024; | ||
| 737 | params.initial_max_stream_data_bidi_remote = 256 * 1024; | ||
| 738 | params.initial_max_data = 1024 * 1024; | ||
| 739 | return params; | ||
| 740 | } | ||
| 741 | |||
| 742 | // --------------------------------------------------------------------------- | ||
| 654 | // Paths | 743 | // Paths |
| 655 | // --------------------------------------------------------------------------- | 744 | // --------------------------------------------------------------------------- |
| 656 | 745 | ||
src/server/quic_server.zig
| Old | New | ||
|---|---|---|---|
| @@ -1002,11 +1002,7 @@ fn pskClientCb( | |||
| 1002 | ) callconv(.c) c_uint { | 1002 | ) callconv(.c) c_uint { |
| 1003 | _ = ssl; | 1003 | _ = ssl; |
| 1004 | _ = hint; | 1004 | _ = hint; |
| 1005 | if (id_max < 4 or key_max < quic.key_len) return 0; | 1005 | return quic.answerPsk(g_client_key, identity, id_max, key_out, key_max, ciphersuite); |
| 1006 | @memcpy(identity[0..4], "mux\x00"); | ||
| 1007 | @memcpy(key_out[0..quic.key_len], &g_client_key.bytes); | ||
| 1008 | if (ciphersuite) |cs| cs.* = quic.psk_ciphersuite; | ||
| 1009 | return quic.key_len; | ||
| 1010 | } | 1006 | } |
| 1011 | 1007 | ||
| 1012 | /// Test scaffolding, exported so the daemon's own tests can drive a real | 1008 | /// Test scaffolding, exported so the daemon's own tests can drive a real |
| @@ -1123,17 +1119,12 @@ pub const TestClient = struct { | |||
| 1123 | } | 1119 | } |
| 1124 | 1120 | ||
| 1125 | pub fn start(self: *TestClient) !void { | 1121 | pub fn start(self: *TestClient) !void { |
| 1126 | if (c.wolfSSL_Init() != c.WOLFSSL_SUCCESS) return error.TlsInit; | 1122 | // Set before clientTls, which hands wolfSSL a pointer to it. |
| 1127 | const ctx = c.wolfSSL_CTX_new(c.wolfTLSv1_3_client_method()) orelse return error.TlsInit; | ||
| 1128 | self.ssl_ctx = ctx; | ||
| 1129 | if (c.ngtcp2_crypto_wolfssl_configure_client_context(ctx) != 0) return error.TlsInit; | ||
| 1130 | c.wolfSSL_CTX_set_psk_client_tls13_callback(ctx, pskClientCb); | ||
| 1131 | _ = c.wolfSSL_CTX_set_cipher_list(ctx, quic.psk_ciphersuite); | ||
| 1132 | const ssl = c.wolfSSL_new(ctx) orelse return error.TlsInit; | ||
| 1133 | self.ssl = ssl; | ||
| 1134 | self.conn_ref = .{ .get_conn = getConn, .user_data = self }; | 1123 | self.conn_ref = .{ .get_conn = getConn, .user_data = self }; |
| 1135 | _ = c.wolfSSL_set_app_data(ssl, &self.conn_ref); | 1124 | const tls = try quic.clientTls(pskClientCb, &self.conn_ref); |
| 1136 | _ = c.wolfSSL_UseALPN(ssl, @constCast(quic.alpn[1..].ptr), quic.alpn.len - 1, c.WOLFSSL_ALPN_FAILED_ON_MISMATCH); | 1125 | self.ssl_ctx = tls.ctx; |
| 1126 | const ssl = tls.ssl; | ||
| 1127 | self.ssl = ssl; | ||
| 1137 | 1128 | ||
| 1138 | var dcid: c.ngtcp2_cid = undefined; | 1129 | var dcid: c.ngtcp2_cid = undefined; |
| 1139 | dcid.datalen = 16; | 1130 | dcid.datalen = 16; |
| @@ -1142,20 +1133,7 @@ pub const TestClient = struct { | |||
| 1142 | scid.datalen = 8; | 1133 | scid.datalen = 8; |
| 1143 | std.crypto.random.bytes(scid.data[0..8]); | 1134 | std.crypto.random.bytes(scid.data[0..8]); |
| 1144 | 1135 | ||
| 1145 | var cbs: c.ngtcp2_callbacks = std.mem.zeroes(c.ngtcp2_callbacks); | 1136 | var cbs = quic.clientCallbacks(); |
| 1146 | cbs.client_initial = c.ngtcp2_crypto_client_initial_cb; | ||
| 1147 | cbs.recv_crypto_data = c.ngtcp2_crypto_recv_crypto_data_cb; | ||
| 1148 | cbs.encrypt = c.ngtcp2_crypto_encrypt_cb; | ||
| 1149 | cbs.decrypt = c.ngtcp2_crypto_decrypt_cb; | ||
| 1150 | cbs.hp_mask = c.ngtcp2_crypto_hp_mask_cb; | ||
| 1151 | cbs.recv_retry = c.ngtcp2_crypto_recv_retry_cb; | ||
| 1152 | cbs.update_key = c.ngtcp2_crypto_update_key_cb; | ||
| 1153 | cbs.delete_crypto_aead_ctx = c.ngtcp2_crypto_delete_crypto_aead_ctx_cb; | ||
| 1154 | cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb; | ||
| 1155 | cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb; | ||
| 1156 | cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb; | ||
| 1157 | cbs.rand = quic.randCb; | ||
| 1158 | cbs.get_new_connection_id2 = quic.getNewCidCb; | ||
| 1159 | cbs.handshake_completed = onHandshake; | 1137 | cbs.handshake_completed = onHandshake; |
| 1160 | cbs.extend_max_local_streams_bidi = onStreams; | 1138 | cbs.extend_max_local_streams_bidi = onStreams; |
| 1161 | cbs.recv_stream_data = onData; | 1139 | cbs.recv_stream_data = onData; |
| @@ -1163,12 +1141,7 @@ pub const TestClient = struct { | |||
| 1163 | var settings: c.ngtcp2_settings = undefined; | 1141 | var settings: c.ngtcp2_settings = undefined; |
| 1164 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); | 1142 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); |
| 1165 | settings.initial_ts = quic.timestampNs(); | 1143 | settings.initial_ts = quic.timestampNs(); |
| 1166 | var params: c.ngtcp2_transport_params = undefined; | 1144 | var params = quic.clientParams(); |
| 1167 | c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, ¶ms); | ||
| 1168 | params.initial_max_streams_bidi = 4; | ||
| 1169 | params.initial_max_stream_data_bidi_local = 256 * 1024; | ||
| 1170 | params.initial_max_stream_data_bidi_remote = 256 * 1024; | ||
| 1171 | params.initial_max_data = 1024 * 1024; | ||
| 1172 | 1145 | ||
| 1173 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | 1146 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); |
| 1174 | var conn: ?*c.ngtcp2_conn = null; | 1147 | var conn: ?*c.ngtcp2_conn = null; |