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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -1
- data/LICENSE-DEPENDENCIES.txt +101 -0
- data/README.md +69 -6
- data/examples/doq_demo.rb +122 -0
- data/examples/echo_demo.rb +23 -12
- data/examples/handshake_demo.rb +18 -20
- data/examples/io_loop_demo.rb +144 -0
- data/examples/verify_demo.rb +105 -0
- data/ext/quic/connection_client.c +403 -68
- data/ext/quic/extconf.rb +138 -28
- data/ext/quic/patches/picotls/0001-openssl-use-raw-public-key-api-for-X25519.patch +74 -0
- data/ext/quic/quic.c +63 -51
- data/ext/quic/quic.h +35 -25
- data/ext/quic/stream.c +59 -23
- data/ext/quic/stream.h +7 -3
- data/lib/quic/connection.rb +69 -9
- data/lib/quic/settings.rb +23 -3
- data/lib/quic/stream.rb +17 -14
- data/lib/quic/transport_params.rb +1 -1
- data/lib/quic/version.rb +2 -2
- data/lib/quic.rb +1 -1
- data/sig/quic.rbs +42 -5
- metadata +31 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
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
|
|
32
|
-
|
|
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->
|
|
42
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
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 +=
|
|
556
|
+
total += (size_t)len;
|
|
380
557
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
765
|
-
{
|
|
766
|
-
|
|
767
|
-
rb_define_alloc_func(
|
|
768
|
-
rb_define_singleton_method(
|
|
769
|
-
rb_define_method(
|
|
770
|
-
rb_define_method(
|
|
771
|
-
rb_define_method(
|
|
772
|
-
rb_define_method(
|
|
773
|
-
rb_define_method(
|
|
774
|
-
rb_define_method(
|
|
775
|
-
rb_define_method(
|
|
776
|
-
rb_define_method(
|
|
777
|
-
rb_define_method(
|
|
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
|
}
|