quic 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,30 +6,89 @@
6
6
  #include <sys/socket.h>
7
7
  #include <time.h>
8
8
 
9
+ #include <openssl/err.h>
9
10
  #include <openssl/rand.h>
10
11
 
11
12
  /* Buffer size for #write_pkt. NGTCP2_MAX_UDP_PAYLOAD_SIZE (1200) is the
12
13
  minimum destlen ngtcp2 accepts. Revisit when PMTUD is enabled (Phase 4+). */
13
14
  #define QUIC_WRITE_PKT_BUFLEN NGTCP2_MAX_UDP_PAYLOAD_SIZE
14
15
 
15
- static int quic_crypto_initialized = 0;
16
+ /* picotls sends a key share only for the first entry; the others are offered
17
+ in supported_groups and reached via HelloRetryRequest. X25519 is first so
18
+ the common case stays 1-RTT. */
19
+ static ptls_key_exchange_algorithm_t *quic_key_exchanges[] = {
20
+ &ptls_openssl_x25519,
21
+ &ptls_openssl_secp256r1,
22
+ #if PTLS_OPENSSL_HAVE_SECP384R1
23
+ &ptls_openssl_secp384r1,
24
+ #endif
25
+ NULL,
26
+ };
27
+
28
+ static ptls_cipher_suite_t *quic_cipher_suites[] = {
29
+ &ptls_openssl_aes128gcmsha256,
30
+ &ptls_openssl_aes256gcmsha384,
31
+ #if PTLS_OPENSSL_HAVE_CHACHA20_POLY1305
32
+ &ptls_openssl_chacha20poly1305sha256,
33
+ #endif
34
+ NULL,
35
+ };
36
+
37
+ /* The system default store is loaded once per process: reading the CA bundle
38
+ takes several milliseconds. X509_STORE is reference counted and safe to
39
+ share across verifications; creation runs under the GVL. Never freed. */
40
+ static X509_STORE *quic_default_store;
41
+
42
+ static X509_STORE *
43
+ quic_get_default_store(void)
44
+ {
45
+ if (quic_default_store == NULL) {
46
+ X509_STORE *store = X509_STORE_new();
47
+ if (store == NULL || X509_STORE_set_default_paths(store) != 1) {
48
+ X509_STORE_free(store);
49
+ rb_raise(rb_eRuntimeError, "failed to load the default certificate store");
50
+ }
51
+ quic_default_store = store;
52
+ }
53
+ return quic_default_store;
54
+ }
16
55
 
17
56
  typedef struct {
18
57
  ngtcp2_conn *conn;
19
- SSL_CTX *ssl_ctx;
20
- SSL *ssl;
58
+ /* Referenced by cptls.ptls, which only keeps the pointer, so this has to
59
+ outlive the ptls_t. */
60
+ ptls_context_t tls_ctx;
61
+ /* Referenced by tls_ctx.verify_certificate when verify_mode is :peer. Holds
62
+ a reference to an X509_STORE, released in quic_client_free. */
63
+ ptls_openssl_verify_certificate_t verify_cert;
64
+ bool verify_cert_initialized;
65
+ /* Filled by quic_override_verify_cb during the handshake so that a failed
66
+ read_pkt can raise CertificateVerifyFailed with the X509 error. */
67
+ bool verify_failed;
68
+ int verify_result; /* X509_V_ERR_*, 0 when the server sent no certificate */
69
+ /* The TLS native handle handed to ngtcp2 (&cptls, not cptls.ptls). */
70
+ ngtcp2_crypto_picotls_ctx cptls;
71
+ /* [0]: QUIC transport params, filled in by ngtcp2; [1]: terminator. */
72
+ ptls_raw_extension_t tls_exts[2];
73
+ /* ALPN list handed to picotls, which keeps the pointers for the whole
74
+ handshake (unlike SSL_set_alpn_protos, which copies). The entries point
75
+ into alpn_buf, and both are owned by this struct. */
76
+ ptls_iovec_t *alpn;
77
+ size_t alpn_count;
78
+ uint8_t *alpn_buf;
21
79
  ngtcp2_cid scid;
22
80
  ngtcp2_cid dcid;
23
81
  /* ngtcp2 1.x requires applications to associate an ngtcp2_conn with the
24
- TLS object via SSL_set_app_data so that crypto callbacks (e.g.
25
- add_handshake_data) can recover the conn. The ref must outlive ssl. */
82
+ TLS object so that crypto callbacks (e.g. add_handshake_data) can recover
83
+ the conn. picotls carries it in the ptls_t's data pointer, set via
84
+ ptls_get_data_ptr. The ref must outlive the ptls_t. */
26
85
  ngtcp2_crypto_conn_ref conn_ref;
27
- /* Back-reference to the Quic::Connection::Client Ruby object that owns
86
+ /* Back-reference to the QUIC::Connection::Client Ruby object that owns
28
87
  this struct. ngtcp2 stream callbacks receive a void* user_data equal
29
88
  to this struct, and they look up @streams via owner. We are stored
30
89
  INSIDE owner via TypedData_Wrap_Struct, so owner is guaranteed alive
31
- while we exist (no dmark needed). GC.compact may relocate owner and
32
- leave this field stale; a dcompact slot is a Phase 5 follow-up. */
90
+ while we exist (no dmark needed). GC.compact may relocate owner, so
91
+ quic_client_compact updates this field via rb_gc_location. */
33
92
  VALUE owner;
34
93
  } quic_client_t;
35
94
 
@@ -38,8 +97,15 @@ quic_client_free(void *ptr)
38
97
  {
39
98
  quic_client_t *c = (quic_client_t *)ptr;
40
99
  if (c->conn) ngtcp2_conn_del(c->conn);
41
- if (c->ssl) SSL_free(c->ssl);
42
- if (c->ssl_ctx) SSL_CTX_free(c->ssl_ctx);
100
+ if (c->cptls.ptls) {
101
+ /* Safe even when configure_client_session never ran or failed midway. */
102
+ ngtcp2_crypto_picotls_deconfigure_session(&c->cptls);
103
+ ptls_free(c->cptls.ptls);
104
+ }
105
+ /* After ptls_free: the ptls_t references verify_cert through tls_ctx. */
106
+ if (c->verify_cert_initialized) ptls_openssl_dispose_verify_certificate(&c->verify_cert);
107
+ xfree(c->alpn);
108
+ xfree(c->alpn_buf);
43
109
  xfree(c);
44
110
  }
45
111
 
@@ -50,9 +116,20 @@ quic_client_size(const void *ptr)
50
116
  return sizeof(quic_client_t);
51
117
  }
52
118
 
119
+ /* GC.compact may relocate the owner Client object. Follow it so the
120
+ void* user_data ngtcp2 callbacks receive (which equals this struct,
121
+ stored inside owner) keeps resolving @streams / @accept_queue. The
122
+ raw ngtcp2/picotls pointers are outside Ruby's heap and are left alone. */
123
+ static void
124
+ quic_client_compact(void *ptr)
125
+ {
126
+ quic_client_t *c = (quic_client_t *)ptr;
127
+ c->owner = rb_gc_location(c->owner);
128
+ }
129
+
53
130
  static const rb_data_type_t quic_client_data_type = {
54
- "Quic::Connection::Client",
55
- {NULL, quic_client_free, quic_client_size,},
131
+ "QUIC::Connection::Client",
132
+ {NULL, quic_client_free, quic_client_size, quic_client_compact,},
56
133
  NULL, NULL,
57
134
  RUBY_TYPED_FREE_IMMEDIATELY,
58
135
  };
@@ -177,7 +254,7 @@ quic_fill_settings(ngtcp2_settings *settings, VALUE st)
177
254
  settings->no_pmtud = RTEST(no_pmtud) ? 1 : 0;
178
255
  }
179
256
 
180
- /* Shared helper: look up the Quic::Stream registered for stream_id in the
257
+ /* Shared helper: look up the QUIC::Stream registered for stream_id in the
181
258
  owner Client's @streams Hash. Returns Qnil if not found (which can happen
182
259
  if the stream was already removed by a previous stream_close callback). */
183
260
  static VALUE
@@ -221,6 +298,15 @@ quic_recv_stream_data_cb(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id,
221
298
  quic_stream_t *s;
222
299
  TypedData_Get_Struct(stream, quic_stream_t, &quic_stream_data_type, s);
223
300
 
301
+ /* Surface peer-initiated (server) streams to #accept_stream. For a client
302
+ connection the low stream-id bit marks the initiator: 1 == server. Push
303
+ once, on first data arrival, then mark so later data does not re-enqueue. */
304
+ if (!s->accept_queued && (stream_id & 0x01)) {
305
+ VALUE accept_queue = rb_ivar_get(c->owner, rb_intern("@accept_queue"));
306
+ rb_ary_push(accept_queue, stream);
307
+ s->accept_queued = true;
308
+ }
309
+
224
310
  if (datalen > 0) {
225
311
  VALUE recv_buffer = rb_ivar_get(stream, rb_intern("@recv_buffer"));
226
312
  rb_str_buf_cat(recv_buffer, (const char *)data, (long)datalen);
@@ -308,6 +394,117 @@ quic_stream_reset_cb(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size,
308
394
  return 0;
309
395
  }
310
396
 
397
+ /* Record the X509 verification result so read_pkt can raise a descriptive
398
+ error. The verdict itself is left to picotls: ret is returned unchanged.
399
+ Runs synchronously inside read_pkt with the GVL held; it only writes to the
400
+ struct and never touches Ruby objects. */
401
+ static int
402
+ quic_override_verify_cb(ptls_openssl_override_verify_certificate_t *self,
403
+ ptls_t *tls, int ret, int ossl_ret, X509 *cert,
404
+ STACK_OF(X509) *chain)
405
+ {
406
+ (void)self;
407
+ (void)cert;
408
+ (void)chain;
409
+ ngtcp2_crypto_conn_ref *ref = *ptls_get_data_ptr(tls);
410
+ quic_client_t *c = (quic_client_t *)ref->user_data;
411
+ if (ret != 0) {
412
+ c->verify_failed = true;
413
+ c->verify_result = ossl_ret;
414
+ }
415
+ return ret;
416
+ }
417
+
418
+ static ptls_openssl_override_verify_certificate_t quic_override_verify = {
419
+ quic_override_verify_cb,
420
+ };
421
+
422
+ /* Validate a ca_file / ca_path setting and return it as a String, or Qnil.
423
+ Accepts a String or anything responding to #to_path. Every misconfiguration
424
+ of the verification settings raises ArgumentError, including a wrong type. */
425
+ static VALUE
426
+ quic_verify_path_value(VALUE value, const char *name)
427
+ {
428
+ if (NIL_P(value)) return Qnil;
429
+
430
+ VALUE path = value;
431
+ if (!RB_TYPE_P(path, T_STRING)) {
432
+ if (rb_respond_to(path, rb_intern("to_path"))) {
433
+ path = rb_funcall(path, rb_intern("to_path"), 0);
434
+ }
435
+ if (!RB_TYPE_P(path, T_STRING)) {
436
+ rb_raise(rb_eArgError, "%s must be a String or Pathname (got %"PRIsVALUE")",
437
+ name, rb_obj_class(value));
438
+ }
439
+ }
440
+ /* Raises ArgumentError when the path contains a NUL byte. */
441
+ StringValueCStr(path);
442
+ return path;
443
+ }
444
+
445
+ /* Install picotls's certificate verifier on c->tls_ctx according to the
446
+ verify_mode / ca_file / ca_path settings. Leaves verify_certificate NULL
447
+ for verify_mode :none, which skips verification altogether. */
448
+ static void
449
+ quic_client_setup_verify(quic_client_t *c, VALUE settings_v, VALUE server_name)
450
+ {
451
+ VALUE mode = rb_funcall(settings_v, rb_intern("verify_mode"), 0);
452
+ /* ca_file and ca_path are not even type-checked under :none. */
453
+ if (mode == ID2SYM(rb_intern("none"))) return;
454
+ if (mode != ID2SYM(rb_intern("peer"))) {
455
+ rb_raise(rb_eArgError, "verify_mode must be :peer or :none (got %+"PRIsVALUE")", mode);
456
+ }
457
+
458
+ VALUE ca_file = quic_verify_path_value(rb_funcall(settings_v, rb_intern("ca_file"), 0), "ca_file");
459
+ VALUE ca_path = quic_verify_path_value(rb_funcall(settings_v, rb_intern("ca_path"), 0), "ca_path");
460
+
461
+ /* An empty name makes OpenSSL clear its host list and skip the name check,
462
+ and both SNI and the check would silently stop at an embedded NUL. */
463
+ if (RSTRING_LEN(server_name) == 0 ||
464
+ memchr(RSTRING_PTR(server_name), '\0', (size_t)RSTRING_LEN(server_name)) != NULL) {
465
+ rb_raise(rb_eArgError,
466
+ "server_name must be a non-empty string without NUL bytes when verify_mode is :peer");
467
+ }
468
+ /* X509_LOOKUP_hash_dir does not check that the directory exists. */
469
+ if (!NIL_P(ca_path) && !RTEST(rb_funcall(rb_cFile, rb_intern("directory?"), 1, ca_path))) {
470
+ rb_raise(rb_eArgError, "ca_path is not a directory: %"PRIsVALUE, ca_path);
471
+ }
472
+
473
+ X509_STORE *store;
474
+ bool own_store = !NIL_P(ca_file) || !NIL_P(ca_path);
475
+ if (own_store) {
476
+ /* Trust only what was given; the system store is not consulted. */
477
+ store = X509_STORE_new();
478
+ if (store == NULL) {
479
+ rb_raise(rb_eRuntimeError, "X509_STORE_new failed");
480
+ }
481
+ if (X509_STORE_load_locations(store,
482
+ NIL_P(ca_file) ? NULL : RSTRING_PTR(ca_file),
483
+ NIL_P(ca_path) ? NULL : RSTRING_PTR(ca_path)) != 1) {
484
+ X509_STORE_free(store);
485
+ /* libcrypto is shared with Ruby's openssl extension; do not leave our
486
+ failure on its error queue. */
487
+ ERR_clear_error();
488
+ if (NIL_P(ca_file)) {
489
+ rb_raise(rb_eArgError, "failed to load ca_path: %"PRIsVALUE, ca_path);
490
+ }
491
+ rb_raise(rb_eArgError, "failed to load ca_file: %"PRIsVALUE, ca_file);
492
+ }
493
+ } else {
494
+ store = quic_get_default_store();
495
+ }
496
+
497
+ /* init takes its own reference on the store, so ours can go right away. */
498
+ int rv = ptls_openssl_init_verify_certificate(&c->verify_cert, store);
499
+ if (own_store) X509_STORE_free(store);
500
+ if (rv != 0) {
501
+ rb_raise(rb_eRuntimeError, "ptls_openssl_init_verify_certificate failed");
502
+ }
503
+ c->verify_cert_initialized = true;
504
+ c->verify_cert.override_callback = &quic_override_verify;
505
+ c->tls_ctx.verify_certificate = &c->verify_cert.super;
506
+ }
507
+
311
508
  static VALUE
312
509
  quic_client_open(int argc, VALUE *argv, VALUE klass)
313
510
  {
@@ -334,13 +531,6 @@ quic_client_open(int argc, VALUE *argv, VALUE klass)
334
531
  quic_require_binary(remote_sockaddr, "remote_sockaddr");
335
532
  Check_Type(server_name, T_STRING);
336
533
 
337
- if (!quic_crypto_initialized) {
338
- if (ngtcp2_crypto_quictls_init() != 0) {
339
- rb_raise(rb_eRuntimeError, "ngtcp2_crypto_quictls_init failed");
340
- }
341
- quic_crypto_initialized = 1;
342
- }
343
-
344
534
  VALUE self = quic_client_alloc(klass);
345
535
  rb_ivar_set(self, rb_intern("@server_name"), server_name);
346
536
 
@@ -348,26 +538,13 @@ quic_client_open(int argc, VALUE *argv, VALUE klass)
348
538
  TypedData_Get_Struct(self, quic_client_t, &quic_client_data_type, c);
349
539
  c->owner = self;
350
540
 
351
- c->ssl_ctx = SSL_CTX_new(TLS_client_method());
352
- if (!c->ssl_ctx) {
353
- rb_raise(rb_eRuntimeError, "SSL_CTX_new failed");
354
- }
355
- if (ngtcp2_crypto_quictls_configure_client_context(c->ssl_ctx) != 0) {
356
- rb_raise(rb_eRuntimeError, "ngtcp2_crypto_quictls_configure_client_context failed");
357
- }
358
-
359
- c->ssl = SSL_new(c->ssl_ctx);
360
- if (!c->ssl) {
361
- rb_raise(rb_eRuntimeError, "SSL_new failed");
362
- }
363
- SSL_set_connect_state(c->ssl);
364
- SSL_set_tlsext_host_name(c->ssl, RSTRING_PTR(server_name));
365
-
541
+ /* Copy the ALPN list into memory owned by c: picotls keeps the pointers
542
+ for the whole handshake, so ALLOCA_N would not survive. An empty list
543
+ means no ALPN extension, matching the old skip-SSL_set_alpn_protos path. */
366
544
  VALUE alpn_ary = rb_funcall(settings_v, rb_intern("alpn"), 0);
367
545
  Check_Type(alpn_ary, T_ARRAY);
368
546
  long n_alpn = RARRAY_LEN(alpn_ary);
369
547
  if (n_alpn > 0) {
370
- /* RFC 7301 wire format: 1-byte length prefix + bytes, concatenated. */
371
548
  size_t total = 0;
372
549
  for (long i = 0; i < n_alpn; i++) {
373
550
  VALUE entry = RARRAY_AREF(alpn_ary, i);
@@ -376,30 +553,44 @@ quic_client_open(int argc, VALUE *argv, VALUE klass)
376
553
  if (len < 1 || len > 255) {
377
554
  rb_raise(rb_eArgError, "alpn entry must be 1-255 bytes (got %ld)", len);
378
555
  }
379
- total += 1 + (size_t)len;
556
+ total += (size_t)len;
380
557
  }
381
- /* total <= 256 * 256 = 65536; ALLOCA_N is safe at this size. */
382
- unsigned char *wire = ALLOCA_N(unsigned char, total);
383
- unsigned char *p = wire;
558
+ c->alpn_buf = ALLOC_N(uint8_t, total);
559
+ c->alpn = ALLOC_N(ptls_iovec_t, n_alpn);
560
+ uint8_t *p = c->alpn_buf;
384
561
  for (long i = 0; i < n_alpn; i++) {
385
562
  VALUE entry = RARRAY_AREF(alpn_ary, i);
386
- long len = RSTRING_LEN(entry);
387
- *p++ = (unsigned char)len;
388
- memcpy(p, RSTRING_PTR(entry), (size_t)len);
563
+ size_t len = (size_t)RSTRING_LEN(entry);
564
+ memcpy(p, RSTRING_PTR(entry), len);
565
+ c->alpn[i] = ptls_iovec_init(p, len);
389
566
  p += len;
390
567
  }
391
- /* SSL_set_alpn_protos returns 0 on success (counter-intuitive). */
392
- if (SSL_set_alpn_protos(c->ssl, wire, (unsigned int)total) != 0) {
393
- rb_raise(rb_eRuntimeError, "SSL_set_alpn_protos failed");
394
- }
568
+ c->alpn_count = (size_t)n_alpn;
395
569
  }
396
570
 
397
- /* Link the SSL handle back to this client so ngtcp2's crypto callbacks
398
- (add_handshake_data, set_encryption_secrets, ...) can resolve the
399
- ngtcp2_conn via SSL_get_app_data -> ngtcp2_crypto_conn_ref. */
571
+ /* quic_client_alloc already zeroed the struct, but calling ctx_init keeps
572
+ the ngtcp2-side initialization explicit and the TLS setup in one place. */
573
+ ngtcp2_crypto_picotls_ctx_init(&c->cptls);
574
+ c->tls_ctx = (ptls_context_t){
575
+ .random_bytes = ptls_openssl_random_bytes,
576
+ .get_time = &ptls_get_time,
577
+ .key_exchanges = quic_key_exchanges,
578
+ .cipher_suites = quic_cipher_suites,
579
+ /* No session resumption yet, so this has no effect today. Keep it so that
580
+ when resumption is added, PSK-only (non-forward-secret) resumption is
581
+ refused rather than silently accepted. */
582
+ .require_dhe_on_psk = 1,
583
+ };
584
+ if (ngtcp2_crypto_picotls_configure_client_context(&c->tls_ctx) != 0) {
585
+ rb_raise(rb_eRuntimeError, "ngtcp2_crypto_picotls_configure_client_context failed");
586
+ }
587
+
588
+ /* ptls_client_new keeps a pointer to tls_ctx, so the verifier has to be in
589
+ place before it runs. */
590
+ quic_client_setup_verify(c, settings_v, server_name);
591
+
400
592
  c->conn_ref.get_conn = quic_client_get_conn;
401
593
  c->conn_ref.user_data = c;
402
- SSL_set_app_data(c->ssl, &c->conn_ref);
403
594
 
404
595
  c->scid.datalen = 8;
405
596
  if (RAND_bytes(c->scid.data, 8) != 1) {
@@ -450,13 +641,42 @@ quic_client_open(int argc, VALUE *argv, VALUE klass)
450
641
  quic_raise_ngtcp2_error(rv);
451
642
  }
452
643
 
453
- ngtcp2_conn_set_tls_native_handle(c->conn, c->ssl);
644
+ /* The TLS session is built last: configure_client_session derives the QUIC
645
+ transport parameters from the conn, so it needs the conn to exist. */
646
+ c->cptls.ptls = ptls_client_new(&c->tls_ctx);
647
+ if (c->cptls.ptls == NULL) {
648
+ rb_raise(rb_eRuntimeError, "ptls_client_new failed");
649
+ }
650
+ /* Let ngtcp2's crypto callbacks (add_handshake_data,
651
+ set_encryption_secrets, ...) resolve the ngtcp2_conn from the ptls_t. */
652
+ *ptls_get_data_ptr(c->cptls.ptls) = &c->conn_ref;
653
+ /* [0] is filled in by configure_client_session with the QUIC transport
654
+ parameters; [1] terminates the list. */
655
+ c->tls_exts[0].type = UINT16_MAX;
656
+ c->tls_exts[1].type = UINT16_MAX;
657
+ c->cptls.handshake_properties.additional_extensions = c->tls_exts;
658
+ if (ngtcp2_crypto_picotls_configure_client_session(&c->cptls, c->conn) != 0) {
659
+ rb_raise(rb_eRuntimeError, "ngtcp2_crypto_picotls_configure_client_session failed");
660
+ }
661
+ c->cptls.handshake_properties.client.negotiated_protocols.list = c->alpn;
662
+ c->cptls.handshake_properties.client.negotiated_protocols.count = c->alpn_count;
663
+ /* ptls_set_server_name copies the name. */
664
+ if (ptls_set_server_name(c->cptls.ptls, RSTRING_PTR(server_name),
665
+ (size_t)RSTRING_LEN(server_name)) != 0) {
666
+ rb_raise(rb_eRuntimeError, "ptls_set_server_name failed");
667
+ }
668
+
669
+ ngtcp2_conn_set_tls_native_handle(c->conn, &c->cptls);
454
670
 
455
- /* Stream registry: stream_id (Integer) -> Quic::Stream.
671
+ /* Stream registry: stream_id (Integer) -> QUIC::Stream.
456
672
  Populated by #open_bidi_stream / #open_uni_stream and the stream_open
457
673
  callback. Entries are removed in the stream_close callback. */
458
674
  rb_ivar_set(self, rb_intern("@streams"), rb_hash_new());
459
675
 
676
+ /* FIFO of peer-initiated (server) streams awaiting #accept_stream. The
677
+ recv_stream_data callback pushes each new server stream here once. */
678
+ rb_ivar_set(self, rb_intern("@accept_queue"), rb_ary_new());
679
+
460
680
  return self;
461
681
  }
462
682
 
@@ -613,6 +833,36 @@ quic_client_write_pkt(int argc, VALUE *argv, VALUE self)
613
833
  return buffer;
614
834
  }
615
835
 
836
+ NORETURN(static void quic_client_raise_error(quic_client_t *c, int rv));
837
+
838
+ /* Like quic_raise_ngtcp2_error, but for errors from read_pkt, where the TLS
839
+ handshake runs: NGTCP2_ERR_CRYPTO carries the TLS alert this client sent,
840
+ and becomes CertificateVerifyFailed when the verifier rejected the peer. */
841
+ static void
842
+ quic_client_raise_error(quic_client_t *c, int rv)
843
+ {
844
+ if (rv != NGTCP2_ERR_CRYPTO) quic_raise_ngtcp2_error(rv);
845
+
846
+ /* 0 means "not set" to ngtcp2; close_notify (0) is never sent for a
847
+ verification failure, so report it as nil. */
848
+ uint8_t alert = ngtcp2_conn_get_tls_alert(c->conn);
849
+ VALUE exc;
850
+ if (c->verify_failed) {
851
+ VALUE msg = c->verify_result
852
+ ? rb_sprintf("certificate verify failed (%s)",
853
+ X509_verify_cert_error_string(c->verify_result))
854
+ : rb_str_new_cstr("certificate verify failed (server sent no certificate)");
855
+ exc = rb_exc_new_str(rb_eQUICErrorCertificateVerifyFailed, msg);
856
+ rb_ivar_set(exc, rb_intern("@verify_result"),
857
+ c->verify_result ? INT2NUM(c->verify_result) : Qnil);
858
+ } else {
859
+ exc = rb_exc_new_cstr(rb_eQUICErrorCryptoError, ngtcp2_strerror(rv));
860
+ }
861
+ rb_ivar_set(exc, rb_intern("@code"), INT2NUM(rv));
862
+ rb_ivar_set(exc, rb_intern("@tls_alert"), alert ? INT2NUM(alert) : Qnil);
863
+ rb_exc_raise(exc);
864
+ }
865
+
616
866
  struct quic_read_pkt_args {
617
867
  quic_client_t *c;
618
868
  VALUE packet;
@@ -630,7 +880,7 @@ quic_read_pkt_body(VALUE arg)
630
880
  quic_now());
631
881
  if (rv != 0) {
632
882
  /* NORETURN: longjmp passes through rb_ensure so unlock still fires. */
633
- quic_raise_ngtcp2_error(rv);
883
+ quic_client_raise_error(a->c, rv);
634
884
  }
635
885
  return Qnil;
636
886
  }
@@ -760,19 +1010,104 @@ quic_client_open_uni_stream(VALUE self)
760
1010
  return stream;
761
1011
  }
762
1012
 
1013
+ /* Send an application CONNECTION_CLOSE (frame type 0x1d) to the peer and
1014
+ transition the underlying ngtcp2_conn to the closing period. One UDP
1015
+ datagram is written to the bound socket; ngtcp2 handles closing-period
1016
+ retransmits via the caller's pump loop. Calling #close on a connection
1017
+ that is already in the closing or draining period is a no-op (= returns
1018
+ nil without doing any I/O). */
1019
+ static VALUE
1020
+ quic_client_close_m(int argc, VALUE *argv, VALUE self)
1021
+ {
1022
+ VALUE opts = Qnil;
1023
+ rb_scan_args(argc, argv, "0:", &opts);
1024
+
1025
+ uint64_t error_code = 0;
1026
+ const uint8_t *reason_ptr = (const uint8_t *)"";
1027
+ size_t reason_len = 0;
1028
+
1029
+ if (!NIL_P(opts)) {
1030
+ VALUE error_code_v = rb_hash_aref(opts, ID2SYM(rb_intern("error_code")));
1031
+ VALUE reason_v = rb_hash_aref(opts, ID2SYM(rb_intern("reason")));
1032
+ if (!NIL_P(error_code_v)) {
1033
+ error_code = NUM2ULL(error_code_v);
1034
+ }
1035
+ if (!NIL_P(reason_v)) {
1036
+ Check_Type(reason_v, T_STRING);
1037
+ reason_ptr = (const uint8_t *)RSTRING_PTR(reason_v);
1038
+ reason_len = (size_t)RSTRING_LEN(reason_v);
1039
+ }
1040
+ }
1041
+
1042
+ quic_client_t *c;
1043
+ TypedData_Get_Struct(self, quic_client_t, &quic_client_data_type, c);
1044
+
1045
+ /* Idempotent: a subsequent #close after the conn already entered the
1046
+ closing/draining period does not emit another CONNECTION_CLOSE. */
1047
+ if (ngtcp2_conn_in_closing_period(c->conn) ||
1048
+ ngtcp2_conn_in_draining_period(c->conn)) {
1049
+ return Qnil;
1050
+ }
1051
+
1052
+ VALUE sock = rb_ivar_get(self, rb_intern("@sock"));
1053
+ if (NIL_P(sock)) {
1054
+ rb_raise(rb_eQUICErrorNotBound,
1055
+ "QUIC::Connection::Client#bind(sock) has not been called");
1056
+ }
1057
+
1058
+ ngtcp2_ccerr ccerr;
1059
+ ngtcp2_ccerr_set_application_error(&ccerr, error_code,
1060
+ reason_len > 0 ? reason_ptr : NULL,
1061
+ reason_len);
1062
+
1063
+ ngtcp2_path_storage path_storage;
1064
+ ngtcp2_path_storage_zero(&path_storage);
1065
+ ngtcp2_pkt_info pi;
1066
+ memset(&pi, 0, sizeof(pi));
1067
+
1068
+ VALUE buffer = rb_str_buf_new(QUIC_WRITE_PKT_BUFLEN);
1069
+ uint8_t *dest = (uint8_t *)RSTRING_PTR(buffer);
1070
+
1071
+ ngtcp2_ssize n = ngtcp2_conn_write_connection_close(
1072
+ c->conn, &path_storage.path, &pi, dest,
1073
+ (size_t)QUIC_WRITE_PKT_BUFLEN, &ccerr, quic_now());
1074
+
1075
+ /* Pre-handshake or otherwise not in a state where CONNECTION_CLOSE can be
1076
+ emitted (ngtcp2 returns NGTCP2_ERR_INVALID_STATE). Spec treats this as
1077
+ best-effort: silently no-op rather than surface an error. The Ruby
1078
+ Client is effectively dead from here. */
1079
+ if (n == NGTCP2_ERR_INVALID_STATE) {
1080
+ return Qnil;
1081
+ }
1082
+
1083
+ if (n < 0) {
1084
+ rb_str_set_len(buffer, 0);
1085
+ quic_raise_ngtcp2_error((int)n);
1086
+ }
1087
+
1088
+ if (n == 0) {
1089
+ return Qnil;
1090
+ }
1091
+
1092
+ rb_str_set_len(buffer, n);
1093
+ rb_funcall(sock, rb_intern("send"), 2, buffer, INT2NUM(0));
1094
+ return Qnil;
1095
+ }
1096
+
763
1097
  void
764
- Init_quic_connection_client(VALUE rb_mQuicConnectionArg)
765
- {
766
- rb_cQuicConnectionClient = rb_define_class_under(rb_mQuicConnectionArg, "Client", rb_cObject);
767
- rb_define_alloc_func(rb_cQuicConnectionClient, quic_client_alloc);
768
- rb_define_singleton_method(rb_cQuicConnectionClient, "_open", quic_client_open, -1);
769
- rb_define_method(rb_cQuicConnectionClient, "write_pkt", quic_client_write_pkt, -1);
770
- rb_define_method(rb_cQuicConnectionClient, "read_pkt", quic_client_read_pkt, -1);
771
- rb_define_method(rb_cQuicConnectionClient, "expiry", quic_client_expiry, 0);
772
- rb_define_method(rb_cQuicConnectionClient, "handle_expiry", quic_client_handle_expiry, 0);
773
- rb_define_method(rb_cQuicConnectionClient, "handshake_completed?", quic_client_handshake_completed_p, 0);
774
- rb_define_method(rb_cQuicConnectionClient, "in_closing_period?", quic_client_in_closing_period_p, 0);
775
- rb_define_method(rb_cQuicConnectionClient, "in_draining_period?", quic_client_in_draining_period_p, 0);
776
- rb_define_method(rb_cQuicConnectionClient, "open_bidi_stream", quic_client_open_bidi_stream, 0);
777
- rb_define_method(rb_cQuicConnectionClient, "open_uni_stream", quic_client_open_uni_stream, 0);
1098
+ Init_quic_connection_client(VALUE rb_mQUICConnectionArg)
1099
+ {
1100
+ rb_cQUICConnectionClient = rb_define_class_under(rb_mQUICConnectionArg, "Client", rb_cObject);
1101
+ rb_define_alloc_func(rb_cQUICConnectionClient, quic_client_alloc);
1102
+ rb_define_singleton_method(rb_cQUICConnectionClient, "_open", quic_client_open, -1);
1103
+ rb_define_method(rb_cQUICConnectionClient, "write_pkt", quic_client_write_pkt, -1);
1104
+ rb_define_method(rb_cQUICConnectionClient, "read_pkt", quic_client_read_pkt, -1);
1105
+ rb_define_method(rb_cQUICConnectionClient, "expiry", quic_client_expiry, 0);
1106
+ rb_define_method(rb_cQUICConnectionClient, "handle_expiry", quic_client_handle_expiry, 0);
1107
+ rb_define_method(rb_cQUICConnectionClient, "handshake_completed?", quic_client_handshake_completed_p, 0);
1108
+ rb_define_method(rb_cQUICConnectionClient, "in_closing_period?", quic_client_in_closing_period_p, 0);
1109
+ rb_define_method(rb_cQUICConnectionClient, "in_draining_period?", quic_client_in_draining_period_p, 0);
1110
+ rb_define_method(rb_cQUICConnectionClient, "open_bidi_stream", quic_client_open_bidi_stream, 0);
1111
+ rb_define_method(rb_cQUICConnectionClient, "open_uni_stream", quic_client_open_uni_stream, 0);
1112
+ rb_define_method(rb_cQUICConnectionClient, "close", quic_client_close_m, -1);
778
1113
  }