a73x

34a3c584

refactor: one owner for the client half of the QUIC handshake

a73x   2026-08-29 01:35

Commit message
refactor: one owner for the client half of the QUIC handshake

quic_client.Client and quic_server.TestClient spelled the same PSK answer,
the same wolfSSL client context and the same ngtcp2 crypto callbacks — the
fixture cannot import the product (folder rule 2), so the vocabulary moves
to the file both already import. Each still sets what is its own.

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, &params);
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, &params);
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, &params);
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;