diff --git a/deps/ngtcp2/lib/includes/ngtcp2/ngtcp2.h b/deps/ngtcp2/lib/includes/ngtcp2/ngtcp2.h index dc1c2836e4..9428fd5f1a 100644 --- a/deps/ngtcp2/lib/includes/ngtcp2/ngtcp2.h +++ b/deps/ngtcp2/lib/includes/ngtcp2/ngtcp2.h @@ -2054,6 +2054,13 @@ NGTCP2_EXTERN ngtcp2_tstamp ngtcp2_conn_get_expiry(ngtcp2_conn *conn); */ NGTCP2_EXTERN ngtcp2_duration ngtcp2_conn_get_idle_timeout(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_pto` returns Probe Timeout (PTO). + */ +NGTCP2_EXTERN ngtcp2_duration ngtcp2_conn_get_pto(ngtcp2_conn *conn); + /** * @function * @@ -2540,7 +2547,9 @@ NGTCP2_EXTERN const ngtcp2_addr *ngtcp2_conn_get_remote_addr(ngtcp2_conn *conn); * @function * * `ngtcp2_conn_initiate_migration` starts connection migration to the - * given |path|. Only client can initiate migration. + * given |path|. Only client can initiate migration. This function + * does immediate migration; it does not probe peer reachability from + * a new local address. * * This function returns 0 if it succeeds, or one of the following * negative error codes: diff --git a/deps/ngtcp2/lib/ngtcp2_acktr.c b/deps/ngtcp2/lib/ngtcp2_acktr.c index 37836f8211..e7403db618 100644 --- a/deps/ngtcp2/lib/ngtcp2_acktr.c +++ b/deps/ngtcp2/lib/ngtcp2_acktr.c @@ -47,13 +47,12 @@ void ngtcp2_acktr_entry_del(ngtcp2_acktr_entry *ent, const ngtcp2_mem *mem) { } static int greater(const ngtcp2_ksl_key *lhs, const ngtcp2_ksl_key *rhs) { - return lhs->i > rhs->i; + return *lhs->i > *rhs->i; } int ngtcp2_acktr_init(ngtcp2_acktr *acktr, ngtcp2_log *log, const ngtcp2_mem *mem) { int rv; - ngtcp2_ksl_key inf_key = {-1}; rv = ngtcp2_ringbuf_init(&acktr->acks, 128, sizeof(ngtcp2_acktr_ack_entry), mem); @@ -61,7 +60,7 @@ int ngtcp2_acktr_init(ngtcp2_acktr *acktr, ngtcp2_log *log, return rv; } - rv = ngtcp2_ksl_init(&acktr->ents, greater, &inf_key, mem); + rv = ngtcp2_ksl_init(&acktr->ents, greater, sizeof(int64_t), mem); if (rv != 0) { ngtcp2_ringbuf_free(&acktr->acks); return rv; @@ -98,9 +97,11 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, ngtcp2_acktr_entry *ent, *prev_ent, *delent; int rv; int added = 0; + ngtcp2_ksl_key key, old_key; if (ngtcp2_ksl_len(&acktr->ents)) { - it = ngtcp2_ksl_lower_bound(&acktr->ents, (const ngtcp2_ksl_key *)&pkt_num); + it = ngtcp2_ksl_lower_bound(&acktr->ents, + ngtcp2_ksl_key_ptr(&key, &pkt_num)); if (ngtcp2_ksl_it_end(&it)) { ngtcp2_ksl_it_prev(&it); ent = ngtcp2_ksl_it_get(&it); @@ -119,8 +120,8 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, if (ngtcp2_ksl_it_begin(&it)) { if (ent->pkt_num + 1 == pkt_num) { ngtcp2_ksl_update_key(&acktr->ents, - (const ngtcp2_ksl_key *)&ent->pkt_num, - (const ngtcp2_ksl_key *)&pkt_num); + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num), + ngtcp2_ksl_key_ptr(&old_key, &pkt_num)); ent->pkt_num = pkt_num; ent->tstamp = ts; ++ent->len; @@ -135,17 +136,14 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, if (ent->pkt_num + 1 == pkt_num) { if (prev_ent->pkt_num == pkt_num + (int64_t)prev_ent->len) { prev_ent->len += ent->len + 1; - rv = ngtcp2_ksl_remove(&acktr->ents, NULL, - (const ngtcp2_ksl_key *)&ent->pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&acktr->ents, NULL, + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num)); ngtcp2_acktr_entry_del(ent, acktr->mem); added = 1; } else { ngtcp2_ksl_update_key(&acktr->ents, - (const ngtcp2_ksl_key *)&ent->pkt_num, - (const ngtcp2_ksl_key *)&pkt_num); + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num), + ngtcp2_ksl_key_ptr(&old_key, &pkt_num)); ent->pkt_num = pkt_num; ent->tstamp = ts; ++ent->len; @@ -165,7 +163,7 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, return rv; } rv = ngtcp2_ksl_insert(&acktr->ents, NULL, - (const ngtcp2_ksl_key *)&ent->pkt_num, ent); + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num), ent); if (rv != 0) { ngtcp2_acktr_entry_del(ent, acktr->mem); return rv; @@ -183,36 +181,28 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, it = ngtcp2_ksl_end(&acktr->ents); ngtcp2_ksl_it_prev(&it); delent = ngtcp2_ksl_it_get(&it); - rv = ngtcp2_ksl_remove(&acktr->ents, NULL, - (const ngtcp2_ksl_key *)&delent->pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&acktr->ents, NULL, + ngtcp2_ksl_key_ptr(&key, &delent->pkt_num)); ngtcp2_acktr_entry_del(delent, acktr->mem); } return 0; } -int ngtcp2_acktr_forget(ngtcp2_acktr *acktr, ngtcp2_acktr_entry *ent) { +void ngtcp2_acktr_forget(ngtcp2_acktr *acktr, ngtcp2_acktr_entry *ent) { ngtcp2_ksl_it it; - int rv; + ngtcp2_ksl_key key; it = ngtcp2_ksl_lower_bound(&acktr->ents, - (const ngtcp2_ksl_key *)&ent->pkt_num); - assert(ngtcp2_ksl_it_key(&it).i == (int64_t)ent->pkt_num); + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num)); + assert(*ngtcp2_ksl_it_key(&it).i == (int64_t)ent->pkt_num); for (; !ngtcp2_ksl_it_end(&it);) { ent = ngtcp2_ksl_it_get(&it); - rv = ngtcp2_ksl_remove(&acktr->ents, &it, - (const ngtcp2_ksl_key *)&ent->pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&acktr->ents, &it, + ngtcp2_ksl_key_ptr(&key, &ent->pkt_num)); ngtcp2_acktr_entry_del(ent, acktr->mem); } - - return 0; } ngtcp2_ksl_it ngtcp2_acktr_get(ngtcp2_acktr *acktr) { @@ -233,34 +223,21 @@ ngtcp2_acktr_ack_entry *ngtcp2_acktr_add_ack(ngtcp2_acktr *acktr, /* * acktr_remove removes |ent| from |acktr|. The iterator which points * to the entry next to |ent| is assigned to |it|. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory. */ -static int acktr_remove(ngtcp2_acktr *acktr, ngtcp2_ksl_it *it, - ngtcp2_acktr_entry *ent) { - int rv; - - rv = ngtcp2_ksl_remove(&acktr->ents, it, - (const ngtcp2_ksl_key *)&ent->pkt_num); - if (rv != 0) { - return rv; - } +static void acktr_remove(ngtcp2_acktr *acktr, ngtcp2_ksl_it *it, + ngtcp2_acktr_entry *ent) { + ngtcp2_ksl_key key; + ngtcp2_ksl_remove(&acktr->ents, it, ngtcp2_ksl_key_ptr(&key, &ent->pkt_num)); ngtcp2_acktr_entry_del(ent, acktr->mem); - - return 0; } -static int acktr_on_ack(ngtcp2_acktr *acktr, ngtcp2_ringbuf *rb, - size_t ack_ent_offset) { +static void acktr_on_ack(ngtcp2_acktr *acktr, ngtcp2_ringbuf *rb, + size_t ack_ent_offset) { ngtcp2_acktr_ack_entry *ack_ent; ngtcp2_acktr_entry *ent; ngtcp2_ksl_it it; - int rv; + ngtcp2_ksl_key key; assert(ngtcp2_ringbuf_len(rb)); @@ -268,13 +245,10 @@ static int acktr_on_ack(ngtcp2_acktr *acktr, ngtcp2_ringbuf *rb, /* Assume that ngtcp2_pkt_validate_ack(fr) returns 0 */ it = ngtcp2_ksl_lower_bound(&acktr->ents, - (const ngtcp2_ksl_key *)&ack_ent->largest_ack); + ngtcp2_ksl_key_ptr(&key, &ack_ent->largest_ack)); for (; !ngtcp2_ksl_it_end(&it);) { ent = ngtcp2_ksl_it_get(&it); - rv = acktr_remove(acktr, &it, ent); - if (rv != 0) { - return rv; - } + acktr_remove(acktr, &it, ent); } if (ngtcp2_ksl_len(&acktr->ents)) { @@ -289,17 +263,14 @@ static int acktr_on_ack(ngtcp2_acktr *acktr, ngtcp2_ringbuf *rb, } ngtcp2_ringbuf_resize(rb, ack_ent_offset); - - return 0; } -int ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { +void ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { ngtcp2_acktr_ack_entry *ent; int64_t largest_ack = fr->largest_ack, min_ack; size_t i, j; ngtcp2_ringbuf *rb = &acktr->acks; size_t nacks = ngtcp2_ringbuf_len(rb); - int rv; /* Assume that ngtcp2_pkt_validate_ack(fr) returns 0 */ for (j = 0; j < nacks; ++j) { @@ -309,17 +280,14 @@ int ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { } } if (j == nacks) { - return 0; + return; } min_ack = largest_ack - (int64_t)fr->first_ack_blklen; if (min_ack <= ent->pkt_num && ent->pkt_num <= largest_ack) { - rv = acktr_on_ack(acktr, rb, j); - if (rv != 0) { - return rv; - } - return 0; + acktr_on_ack(acktr, rb, j); + return; } for (i = 0; i < fr->num_blks && j < nacks; ++i) { @@ -330,7 +298,7 @@ int ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { if (ent->pkt_num > largest_ack) { ++j; if (j == nacks) { - return 0; + return; } ent = ngtcp2_ringbuf_get(rb, j); continue; @@ -338,11 +306,12 @@ int ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { if (ent->pkt_num < min_ack) { break; } - return acktr_on_ack(acktr, rb, j); + acktr_on_ack(acktr, rb, j); + return; } } - return 0; + return; } void ngtcp2_acktr_commit_ack(ngtcp2_acktr *acktr) { diff --git a/deps/ngtcp2/lib/ngtcp2_acktr.h b/deps/ngtcp2/lib/ngtcp2_acktr.h index 585e13b921..35e00d76bd 100644 --- a/deps/ngtcp2/lib/ngtcp2_acktr.h +++ b/deps/ngtcp2/lib/ngtcp2_acktr.h @@ -166,14 +166,8 @@ int ngtcp2_acktr_add(ngtcp2_acktr *acktr, int64_t pkt_num, int active_ack, * ngtcp2_acktr_forget removes all entries which have the packet * number that is equal to or less than ent->pkt_num. This function * assumes that |acktr| includes |ent|. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory. */ -int ngtcp2_acktr_forget(ngtcp2_acktr *acktr, ngtcp2_acktr_entry *ent); +void ngtcp2_acktr_forget(ngtcp2_acktr *acktr, ngtcp2_acktr_entry *ent); /* * ngtcp2_acktr_get returns the pointer to pointer to the entry which @@ -196,16 +190,8 @@ ngtcp2_acktr_add_ack(ngtcp2_acktr *acktr, int64_t pkt_num, int64_t largest_ack); * |pkt_num| is a packet number which includes |fr|. If we receive * ACK which acknowledges the ACKs added by ngtcp2_acktr_add_ack, * ngtcp2_acktr_entry which the outgoing ACK acknowledges is removed. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_CALLBACK_FAILURE - * User-defined callback function failed. - * NGTCP2_ERR_NOMEM - * Out of memory. */ -int ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr); +void ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr); /* * ngtcp2_acktr_commit_ack tells |acktr| that ACK frame is generated. diff --git a/deps/ngtcp2/lib/ngtcp2_cid.c b/deps/ngtcp2/lib/ngtcp2_cid.c index 465859e0ac..c53a61f99a 100644 --- a/deps/ngtcp2/lib/ngtcp2_cid.c +++ b/deps/ngtcp2/lib/ngtcp2_cid.c @@ -86,11 +86,13 @@ void ngtcp2_dcid_init(ngtcp2_dcid *dcid, uint64_t seq, const ngtcp2_cid *cid, memset(dcid->token, 0, NGTCP2_STATELESS_RESET_TOKENLEN); } ngtcp2_path_storage_zero(&dcid->ps); + dcid->ts_retired = UINT64_MAX; } void ngtcp2_dcid_copy(ngtcp2_dcid *dest, const ngtcp2_dcid *src) { ngtcp2_dcid_init(dest, src->seq, &src->cid, src->token); ngtcp2_path_copy(&dest->ps.path, &src->ps.path); + dest->ts_retired = src->ts_retired; } int ngtcp2_dcid_verify_uniqueness(ngtcp2_dcid *dcid, uint64_t seq, diff --git a/deps/ngtcp2/lib/ngtcp2_cid.h b/deps/ngtcp2/lib/ngtcp2_cid.h index 69350fabe9..53d97d7c8e 100644 --- a/deps/ngtcp2/lib/ngtcp2_cid.h +++ b/deps/ngtcp2/lib/ngtcp2_cid.h @@ -65,6 +65,9 @@ typedef struct { /* path is a path which cid is bound to. The addresses are zero length if cid has not been bound to a particular path yet. */ ngtcp2_path_storage ps; + /* ts_retired is the timestamp when peer tells that this CID is + retired. */ + ngtcp2_tstamp ts_retired; /* token is a stateless reset token associated to this CID. Actually, the stateless reset token is tied to the connection, not to the particular connection ID. */ diff --git a/deps/ngtcp2/lib/ngtcp2_conn.c b/deps/ngtcp2/lib/ngtcp2_conn.c index 8263ede22f..46c4c31073 100644 --- a/deps/ngtcp2/lib/ngtcp2_conn.c +++ b/deps/ngtcp2/lib/ngtcp2_conn.c @@ -423,35 +423,6 @@ static void pktns_free(ngtcp2_pktns *pktns, const ngtcp2_mem *mem) { ngtcp2_gaptr_free(&pktns->rx.pngap); } -/* - * inf_cid is used as the "last" key in ngtcp2_ksl. We don't accept - * this as valid connection ID. It is reasonable because it is too - * predictable. - */ -static ngtcp2_cid inf_cid = { - NGTCP2_MAX_CIDLEN, - { - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - }, -}; - static int cid_less(const ngtcp2_ksl_key *lhs, const ngtcp2_ksl_key *rhs) { return ngtcp2_cid_less(lhs->ptr, rhs->ptr); } @@ -517,8 +488,14 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, goto fail_dcid_unused_init; } - rv = ngtcp2_ksl_init(&(*pconn)->scid.set, cid_less, - ngtcp2_ksl_key_ptr(&key, &inf_cid), mem); + rv = + ngtcp2_ringbuf_init(&(*pconn)->dcid.retired, NGTCP2_MAX_DCID_RETIRED_SIZE, + sizeof(ngtcp2_dcid), mem); + if (rv != 0) { + goto fail_dcid_retired_init; + } + + rv = ngtcp2_ksl_init(&(*pconn)->scid.set, cid_less, sizeof(ngtcp2_cid), mem); if (rv != 0) { goto fail_scid_set_init; } @@ -648,6 +625,8 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, delete_scid(&(*pconn)->scid.set, mem); ngtcp2_ksl_free(&(*pconn)->scid.set); fail_scid_set_init: + ngtcp2_ringbuf_free(&(*pconn)->dcid.retired); +fail_dcid_retired_init: ngtcp2_ringbuf_free(&(*pconn)->dcid.unused); fail_dcid_unused_init: ngtcp2_ringbuf_free(&(*pconn)->dcid.bound); @@ -771,6 +750,7 @@ void ngtcp2_conn_del(ngtcp2_conn *conn) { ngtcp2_pq_free(&conn->scid.used); delete_scid(&conn->scid.set, conn->mem); ngtcp2_ksl_free(&conn->scid.set); + ngtcp2_ringbuf_free(&conn->dcid.retired); ngtcp2_ringbuf_free(&conn->dcid.unused); ngtcp2_ringbuf_free(&conn->dcid.bound); @@ -920,10 +900,7 @@ static int conn_create_ack_frame(ngtcp2_conn *conn, ngtcp2_frame **pfr, /* TODO Just remove entries which cannot fit into a single ACK frame for now. */ if (!ngtcp2_ksl_it_end(&it)) { - rv = ngtcp2_acktr_forget(acktr, ngtcp2_ksl_it_get(&it)); - if (rv != 0) { - return rv; - } + ngtcp2_acktr_forget(acktr, ngtcp2_ksl_it_get(&it)); } *pfr = fr; @@ -1044,13 +1021,17 @@ static size_t pktns_select_pkt_numlen(ngtcp2_pktns *pktns) { */ static uint64_t conn_cwnd_left(ngtcp2_conn *conn) { uint64_t bytes_in_flight = ngtcp2_conn_get_bytes_in_flight(conn); + uint64_t cwnd = + conn->pv && (conn->pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE) + ? NGTCP2_MIN_CWND + : conn->ccs.cwnd; - /* We might send more than bytes_in_flight if TLP/RTO packets are + /* We might send more than bytes_in_flight if probe packets are involved. */ - if (bytes_in_flight >= conn->ccs.cwnd) { + if (bytes_in_flight >= cwnd) { return 0; } - return conn->ccs.cwnd - bytes_in_flight; + return cwnd - bytes_in_flight; } /* @@ -1096,7 +1077,7 @@ static int conn_cryptofrq_unacked_pop(ngtcp2_conn *conn, ngtcp2_pktns *pktns, uint64_t offset, end_offset; size_t idx, end_idx; size_t base_offset, end_base_offset; - ngtcp2_psl_it gapit; + ngtcp2_ksl_it gapit; ngtcp2_range gap; ngtcp2_rtb *rtb = &pktns->rtb; ngtcp2_vec *v; @@ -1118,7 +1099,7 @@ static int conn_cryptofrq_unacked_pop(ngtcp2_conn *conn, ngtcp2_pktns *pktns, gapit = ngtcp2_gaptr_get_first_gap_after(&rtb->crypto->tx.acked_offset, offset); - gap = ngtcp2_psl_it_range(&gapit); + gap = *(ngtcp2_range *)ngtcp2_ksl_it_key(&gapit).ptr; if (gap.begin < offset) { gap.begin = offset; } @@ -1559,7 +1540,7 @@ static ssize_t conn_write_handshake_pkt(ngtcp2_conn *conn, uint8_t *dest, } if (ackfr) { - rv = conn_ppe_write_frame(conn, &ppe, &hd, ackfr); + rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, ackfr); if (rv != 0) { assert(NGTCP2_ERR_NOBUF == rv); } else { @@ -1997,7 +1978,7 @@ static int conn_enqueue_new_connection_id(ngtcp2_conn *conn) { return rv; } - if (cid.datalen != cidlen || ngtcp2_cid_eq(&inf_cid, &cid)) { + if (cid.datalen != cidlen) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -2078,6 +2059,7 @@ static int conn_remove_retired_connection_id(ngtcp2_conn *conn, ngtcp2_duration timeout; ngtcp2_scid *scid; ngtcp2_ksl_key key; + ngtcp2_dcid *dcid; int rv; timeout = conn_compute_pto(conn); @@ -2098,15 +2080,20 @@ static int conn_remove_retired_connection_id(ngtcp2_conn *conn, return rv; } - rv = ngtcp2_ksl_remove(&conn->scid.set, NULL, - ngtcp2_ksl_key_ptr(&key, &scid->cid)); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&conn->scid.set, NULL, + ngtcp2_ksl_key_ptr(&key, &scid->cid)); ngtcp2_pq_pop(&conn->scid.used); ngtcp2_mem_free(conn->mem, scid); } + for (; ngtcp2_ringbuf_len(&conn->dcid.retired);) { + dcid = ngtcp2_ringbuf_get(&conn->dcid.retired, 0); + if (dcid->ts_retired + timeout >= ts) { + break; + } + ngtcp2_ringbuf_pop_front(&conn->dcid.retired); + } + return 0; } @@ -2278,7 +2265,7 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, } if (ackfr) { - rv = conn_ppe_write_frame(conn, &ppe, &hd, ackfr); + rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, ackfr); if (rv != 0) { assert(NGTCP2_ERR_NOBUF == rv); } else { @@ -3040,11 +3027,22 @@ static int conn_handshake_remnants_left(ngtcp2_conn *conn) { * NGTCP2_ERR_NOMEM * Out of memory */ -static int conn_retire_dcid(ngtcp2_conn *conn, const ngtcp2_dcid *dcid) { +static int conn_retire_dcid(ngtcp2_conn *conn, const ngtcp2_dcid *dcid, + ngtcp2_tstamp ts) { ngtcp2_pktns *pktns = &conn->pktns; ngtcp2_frame_chain *nfrc; + ngtcp2_ringbuf *rb = &conn->dcid.retired; + ngtcp2_dcid *dest; int rv; + if (ngtcp2_ringbuf_full(rb)) { + ngtcp2_ringbuf_pop_front(rb); + } + + dest = ngtcp2_ringbuf_push_back(rb); + ngtcp2_dcid_copy(dest, dcid); + dest->ts_retired = ts; + rv = ngtcp2_frame_chain_new(&nfrc, conn->mem); if (rv != 0) { return rv; @@ -3069,7 +3067,7 @@ static int conn_retire_dcid(ngtcp2_conn *conn, const ngtcp2_dcid *dcid) { * NGTCP2_ERR_NOMEM * Out of memory */ -static int conn_stop_pv(ngtcp2_conn *conn) { +static int conn_stop_pv(ngtcp2_conn *conn, ngtcp2_tstamp ts) { int rv = 0; ngtcp2_pv *pv = conn->pv; @@ -3078,7 +3076,7 @@ static int conn_stop_pv(ngtcp2_conn *conn) { } if (pv->flags & NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH) { - rv = conn_retire_dcid(conn, &pv->dcid); + rv = conn_retire_dcid(conn, &pv->dcid, ts); } ngtcp2_pv_del(pv); @@ -3099,7 +3097,8 @@ static int conn_stop_pv(ngtcp2_conn *conn) { * NGTCP2_ERR_CALLBACK_FAILURE * User-defined callback function failed. */ -static int conn_on_path_validation_failed(ngtcp2_conn *conn, ngtcp2_pv *pv) { +static int conn_on_path_validation_failed(ngtcp2_conn *conn, ngtcp2_pv *pv, + ngtcp2_tstamp ts) { int rv; /* If path validation fails, the bound DCID is no longer @@ -3114,7 +3113,11 @@ static int conn_on_path_validation_failed(ngtcp2_conn *conn, ngtcp2_pv *pv) { } } - return conn_stop_pv(conn); + if (pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE) { + ngtcp2_dcid_copy(&conn->dcid.current, &pv->fallback_dcid); + } + + return conn_stop_pv(conn, ts); } /* @@ -3142,7 +3145,7 @@ static ssize_t conn_write_path_challenge(ngtcp2_conn *conn, ngtcp2_path *path, if (ngtcp2_pv_validation_timed_out(pv, ts)) { ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PTV, "path validation was timed out"); - return conn_on_path_validation_failed(conn, pv); + return conn_on_path_validation_failed(conn, pv, ts); } ngtcp2_pv_handle_entry_expiry(pv, ts); @@ -3190,7 +3193,7 @@ static ssize_t conn_write_path_challenge(ngtcp2_conn *conn, ngtcp2_path *path, * Out of memory */ static int conn_bind_dcid(ngtcp2_conn *conn, ngtcp2_dcid **pdcid, - const ngtcp2_path *path) { + const ngtcp2_path *path, ngtcp2_tstamp ts) { ngtcp2_pv *pv = conn->pv; ngtcp2_dcid *dcid, *ndcid; size_t i, len; @@ -3216,7 +3219,7 @@ static int conn_bind_dcid(ngtcp2_conn *conn, ngtcp2_dcid **pdcid, dcid = ngtcp2_ringbuf_get(&conn->dcid.unused, 0); if (ngtcp2_ringbuf_full(&conn->dcid.bound)) { - rv = conn_retire_dcid(conn, ngtcp2_ringbuf_get(&conn->dcid.bound, 0)); + rv = conn_retire_dcid(conn, ngtcp2_ringbuf_get(&conn->dcid.bound, 0), ts); if (rv != 0) { return rv; } @@ -3291,7 +3294,7 @@ static ssize_t conn_write_path_response(ngtcp2_conn *conn, ngtcp2_path *path, conn->pv. */ assert(conn->server); - rv = conn_bind_dcid(conn, &dcid, &pcent->ps.path); + rv = conn_bind_dcid(conn, &dcid, &pcent->ps.path, ts); if (rv != 0) { if (ngtcp2_err_is_fatal(rv)) { return rv; @@ -3606,12 +3609,7 @@ static int conn_on_retry(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, /* Just freeing memory is dangerous because we might free twice. */ - rv = ngtcp2_rtb_remove_all(rtb, &frc); - if (rv != 0) { - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_list_del(frc, conn->mem); - return rv; - } + ngtcp2_rtb_remove_all(rtb, &frc); rv = conn_resched_frames(conn, &conn->pktns, &frc); if (rv != 0) { @@ -3621,12 +3619,7 @@ static int conn_on_retry(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, } frc = NULL; - rv = ngtcp2_rtb_remove_all(in_rtb, &frc); - if (rv != 0) { - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_list_del(frc, conn->mem); - return rv; - } + ngtcp2_rtb_remove_all(in_rtb, &frc); rv = conn_resched_frames(conn, &conn->in_pktns, &frc); if (rv != 0) { @@ -3656,13 +3649,7 @@ int ngtcp2_conn_detect_lost_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns, int rv; ngtcp2_duration pto = conn_compute_pto(conn); - rv = ngtcp2_rtb_detect_lost_pkt(&pktns->rtb, &frc, rcs, pto, ts); - if (rv != 0) { - /* TODO assert this */ - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_list_del(frc, conn->mem); - return rv; - } + ngtcp2_rtb_detect_lost_pkt(&pktns->rtb, &frc, rcs, pto, ts); rv = conn_resched_frames(conn, pktns, &frc); if (rv != 0) { @@ -3698,10 +3685,7 @@ static int conn_recv_ack(ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_ack *fr, return rv; } - rv = ngtcp2_acktr_recv_ack(&pktns->acktr, fr); - if (rv != 0) { - return rv; - } + ngtcp2_acktr_recv_ack(&pktns->acktr, fr); num_acked = ngtcp2_rtb_recv_ack(&pktns->rtb, fr, conn, ts); if (num_acked < 0) { @@ -4042,10 +4026,7 @@ static int conn_emit_pending_crypto_data(ngtcp2_conn *conn, return rv; } - rv = ngtcp2_rob_pop(&strm->rx.rob, rx_offset - datalen, datalen); - if (rv != 0) { - return rv; - } + ngtcp2_rob_pop(&strm->rx.rob, rx_offset - datalen, datalen); } } @@ -4066,7 +4047,7 @@ static void conn_recv_path_challenge(ngtcp2_conn *conn, const ngtcp2_path *path, } static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_path *path, - ngtcp2_path_response *fr) { + ngtcp2_path_response *fr, ngtcp2_tstamp ts) { int rv; ngtcp2_pv *pv = conn->pv, *npv = NULL; ngtcp2_duration timeout; @@ -4078,20 +4059,22 @@ static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_path *path, rv = ngtcp2_pv_validate(pv, path, fr->data); if (rv != 0) { if (rv == NGTCP2_ERR_PATH_VALIDATION_FAILED) { - return conn_on_path_validation_failed(conn, pv); + return conn_on_path_validation_failed(conn, pv, ts); } return 0; } - if (pv->flags & NGTCP2_PV_FLAG_VERIFY_OLD_PATH_ON_SUCCESS) { + /* If validation succeeds, we don't have to throw DCID away. */ + pv->flags &= (uint8_t)~NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH; + + if (pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE) { timeout = conn_compute_pto(conn); timeout = ngtcp2_max(timeout, (ngtcp2_duration)(6ULL * NGTCP2_DEFAULT_INITIAL_RTT)); - rv = ngtcp2_pv_new(&npv, &conn->dcid.current, timeout, - NGTCP2_PV_FLAG_DONT_CARE | - NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH, - &conn->log, conn->mem); + rv = ngtcp2_pv_new(&npv, &pv->fallback_dcid, timeout, + NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH, &conn->log, + conn->mem); if (rv != 0) { return rv; } @@ -4100,15 +4083,14 @@ static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_path *path, /* TODO Retire all DCIDs in conn->bound_dcid */ if (!(pv->flags & NGTCP2_PV_FLAG_DONT_CARE)) { - if (!(pv->flags & NGTCP2_PV_FLAG_VERIFY_OLD_PATH_ON_SUCCESS)) { - rv = conn_retire_dcid(conn, &conn->dcid.current); + if (!(pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE)) { + rv = conn_retire_dcid(conn, &conn->dcid.current, ts); if (rv != 0) { goto fail; } + ngtcp2_dcid_copy(&conn->dcid.current, &pv->dcid); } - ngtcp2_dcid_copy(&conn->dcid.current, &pv->dcid); - rv = conn_call_path_validation(conn, &pv->dcid.ps.path, NGTCP2_PATH_VALIDATION_RESULT_SUCCESS); if (rv != 0) { @@ -4116,7 +4098,7 @@ static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_path *path, } } - rv = conn_stop_pv(conn); + rv = conn_stop_pv(conn, ts); if (rv != 0) { goto fail; } @@ -4190,8 +4172,9 @@ static int pktns_pkt_num_is_duplicate(ngtcp2_pktns *pktns, int64_t pkt_num) { */ static int pktns_commit_recv_pkt_num(ngtcp2_pktns *pktns, int64_t pkt_num) { int rv; - ngtcp2_psl_it it; - ngtcp2_range key; + ngtcp2_ksl_it it; + ngtcp2_ksl_key key; + ngtcp2_range range; if (pktns->rx.max_pkt_num + 1 != pkt_num) { ngtcp2_acktr_immediate_ack(&pktns->acktr); @@ -4205,10 +4188,11 @@ static int pktns_commit_recv_pkt_num(ngtcp2_pktns *pktns, int64_t pkt_num) { return rv; } - if (ngtcp2_psl_len(&pktns->rx.pngap.gap) > 256) { - it = ngtcp2_psl_begin(&pktns->rx.pngap.gap); - key = ngtcp2_psl_it_range(&it); - return ngtcp2_psl_remove(&pktns->rx.pngap.gap, NULL, &key); + if (ngtcp2_ksl_len(&pktns->rx.pngap.gap) > 256) { + it = ngtcp2_ksl_begin(&pktns->rx.pngap.gap); + range = *(ngtcp2_range *)ngtcp2_ksl_it_key(&it).ptr; + ngtcp2_ksl_remove(&pktns->rx.pngap.gap, NULL, + ngtcp2_ksl_key_ptr(&key, &range)); } return 0; @@ -4843,10 +4827,7 @@ static int conn_emit_pending_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm, return rv; } - rv = ngtcp2_rob_pop(&strm->rx.rob, rx_offset - datalen, datalen); - if (rv != 0) { - return rv; - } + ngtcp2_rob_pop(&strm->rx.rob, rx_offset - datalen, datalen); } } @@ -5489,7 +5470,10 @@ static int conn_on_stateless_reset(ngtcp2_conn *conn, const uint8_t *payload, } if (ngtcp2_verify_stateless_retry_token(conn->dcid.current.token, - sr.stateless_reset_token) != 0) { + sr.stateless_reset_token) != 0 && + (!conn->pv || !(conn->pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE) || + ngtcp2_verify_stateless_retry_token(conn->pv->fallback_dcid.token, + sr.stateless_reset_token) != 0)) { len = ngtcp2_ringbuf_len(&conn->dcid.bound); for (i = 0; i < len; ++i) { dcid = ngtcp2_ringbuf_get(&conn->dcid.bound, i); @@ -5500,18 +5484,7 @@ static int conn_on_stateless_reset(ngtcp2_conn *conn, const uint8_t *payload, } if (i == len) { - len = ngtcp2_ringbuf_len(&conn->dcid.unused); - for (i = 0; i < len; ++i) { - dcid = ngtcp2_ringbuf_get(&conn->dcid.unused, i); - if (ngtcp2_verify_stateless_retry_token( - dcid->token, sr.stateless_reset_token) == 0) { - break; - } - } - - if (i == len) { - return NGTCP2_ERR_INVALID_ARGUMENT; - } + return NGTCP2_ERR_INVALID_ARGUMENT; } } @@ -5901,7 +5874,8 @@ static void conn_reset_congestion_state(ngtcp2_conn *conn) { * Out of memory */ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, - const ngtcp2_path *path) { + const ngtcp2_path *path, + ngtcp2_tstamp ts) { ngtcp2_dcid *dcid, *last_dcid; ngtcp2_ringbuf *rb; @@ -5912,6 +5886,21 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, assert(conn->server); + if (conn->pv && (conn->pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE) && + ngtcp2_path_eq(&conn->pv->fallback_dcid.ps.path, path)) { + /* If new path equals fallback path, that means connection + migrated back to the original path. Fallback path is + considered to be validated. */ + ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PTV, + "path is migrated back to the original path"); + ngtcp2_dcid_copy(&conn->dcid.current, &conn->pv->fallback_dcid); + rv = conn_stop_pv(conn, ts); + if (rv != 0) { + return rv; + } + return 0; + } + len = ngtcp2_ringbuf_len(&conn->dcid.bound); for (i = 0; i < len; ++i) { @@ -5931,16 +5920,6 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, rb = &conn->dcid.unused; } - if (conn->pv) { - ngtcp2_log_info( - &conn->log, NGTCP2_LOG_EVENT_PTV, - "path migration is aborted because new migration has started"); - rv = conn_stop_pv(conn); - if (rv != 0) { - return rv; - } - } - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "remote address has changed"); @@ -5951,14 +5930,33 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, ngtcp2_max(timeout, (ngtcp2_duration)(6ULL * NGTCP2_DEFAULT_INITIAL_RTT)); rv = ngtcp2_pv_new(&pv, dcid, timeout, - NGTCP2_PV_FLAG_VERIFY_OLD_PATH_ON_SUCCESS, &conn->log, - conn->mem); + NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE | + NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH, + &conn->log, conn->mem); if (rv != 0) { return rv; } - conn->pv = pv; ngtcp2_path_copy(&pv->dcid.ps.path, path); + if (conn->pv && (conn->pv->flags & NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE)) { + ngtcp2_dcid_copy(&pv->fallback_dcid, &conn->pv->fallback_dcid); + } else { + ngtcp2_dcid_copy(&pv->fallback_dcid, &conn->dcid.current); + } + ngtcp2_dcid_copy(&conn->dcid.current, &pv->dcid); + + if (conn->pv) { + ngtcp2_log_info( + &conn->log, NGTCP2_LOG_EVENT_PTV, + "path migration is aborted because new migration has started"); + rv = conn_stop_pv(conn, ts); + if (rv != 0) { + ngtcp2_pv_del(pv); + return rv; + } + } + + conn->pv = pv; if (rb == &conn->dcid.unused) { ngtcp2_ringbuf_pop_front(&conn->dcid.unused); @@ -6396,7 +6394,7 @@ static ssize_t conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, conn_recv_path_challenge(conn, path, &fr->path_challenge); break; case NGTCP2_FRAME_PATH_RESPONSE: - rv = conn_recv_path_response(conn, path, &fr->path_response); + rv = conn_recv_path_response(conn, path, &fr->path_response, ts); if (rv != 0) { return rv; } @@ -6429,7 +6427,7 @@ static ssize_t conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, pktns->rx.max_pkt_num < hd.pkt_num && !ngtcp2_path_eq(&conn->dcid.current.ps.path, path) && !conn_path_validation_in_progress(conn, path)) { - rv = conn_recv_non_probing_pkt_on_new_path(conn, path); + rv = conn_recv_non_probing_pkt_on_new_path(conn, path, ts); if (rv != 0) { if (ngtcp2_err_is_fatal(rv)) { return rv; @@ -6631,6 +6629,24 @@ static int conn_recv_cpkt(ngtcp2_conn *conn, const ngtcp2_path *path, return 0; } +/* + * conn_is_retired_path returns nonzero if |path| is inclued in + * retired path list. + */ +static int conn_is_retired_path(ngtcp2_conn *conn, const ngtcp2_path *path) { + size_t i, len = ngtcp2_ringbuf_len(&conn->dcid.retired); + ngtcp2_dcid *dcid; + + for (i = 0; i < len; ++i) { + dcid = ngtcp2_ringbuf_get(&conn->dcid.retired, i); + if (ngtcp2_path_eq(&dcid->ps.path, path)) { + return 1; + } + } + + return 0; +} + int ngtcp2_conn_read_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, const uint8_t *pkt, size_t pktlen, ngtcp2_tstamp ts) { int rv = 0; @@ -6646,7 +6662,8 @@ int ngtcp2_conn_read_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, /* client does not expect a packet from unknown path. */ if (!conn->server && !ngtcp2_path_eq(&conn->dcid.current.ps.path, path) && - (!conn->pv || !ngtcp2_path_eq(&conn->pv->dcid.ps.path, path))) { + (!conn->pv || !ngtcp2_path_eq(&conn->pv->dcid.ps.path, path)) && + !conn_is_retired_path(conn, path)) { ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "ignore packet from unknown path"); return 0; @@ -6852,8 +6869,8 @@ static int conn_select_preferred_addr(ngtcp2_conn *conn) { timeout = ngtcp2_max(timeout, (ngtcp2_duration)(6ULL * NGTCP2_DEFAULT_INITIAL_RTT)); - rv = ngtcp2_pv_new(&pv, &dcid, timeout, NGTCP2_PV_FLAG_NONE, &conn->log, - conn->mem); + rv = ngtcp2_pv_new(&pv, &dcid, timeout, NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH, + &conn->log, conn->mem); if (rv != 0) { /* TODO Call ngtcp2_dcid_free here if it is introduced */ return rv; @@ -8184,12 +8201,7 @@ int ngtcp2_conn_early_data_rejected(ngtcp2_conn *conn) { conn->flags |= NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED; - rv = ngtcp2_rtb_remove_all(rtb, &frc); - if (rv != 0) { - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_list_del(frc, conn->mem); - return rv; - } + ngtcp2_rtb_remove_all(rtb, &frc); rv = conn_resched_frames(conn, pktns, &frc); if (rv != 0) { @@ -8505,8 +8517,6 @@ int ngtcp2_conn_initiate_migration(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts) { int rv; ngtcp2_dcid *dcid; - ngtcp2_pv *pv; - ngtcp2_duration timeout; assert(!conn->server); @@ -8525,29 +8535,22 @@ int ngtcp2_conn_initiate_migration(ngtcp2_conn *conn, const ngtcp2_path *path, dcid = ngtcp2_ringbuf_get(&conn->dcid.unused, 0); - rv = conn_stop_pv(conn); + rv = conn_stop_pv(conn, ts); if (rv != 0) { return rv; } - timeout = conn_compute_pto(conn); - timeout = - ngtcp2_max(timeout, (ngtcp2_duration)(6ULL * NGTCP2_DEFAULT_INITIAL_RTT)); - - rv = ngtcp2_pv_new(&pv, dcid, timeout, NGTCP2_PV_FLAG_NONE, &conn->log, - conn->mem); + rv = conn_retire_dcid(conn, &conn->dcid.current, ts); if (rv != 0) { return rv; } - conn->pv = pv; - - ngtcp2_path_copy(&pv->dcid.ps.path, path); + ngtcp2_dcid_copy(&conn->dcid.current, dcid); + ngtcp2_path_copy(&conn->dcid.current.ps.path, path); + ngtcp2_ringbuf_pop_front(&conn->dcid.unused); conn_reset_congestion_state(conn); - ngtcp2_ringbuf_pop_front(&conn->dcid.unused); - return 0; } @@ -8572,6 +8575,10 @@ ngtcp2_duration ngtcp2_conn_get_idle_timeout(ngtcp2_conn *conn) { trpto); } +ngtcp2_duration ngtcp2_conn_get_pto(ngtcp2_conn *conn) { + return conn_compute_pto(conn); +} + void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent, const ngtcp2_path *path, const uint8_t *data) { diff --git a/deps/ngtcp2/lib/ngtcp2_conn.h b/deps/ngtcp2/lib/ngtcp2_conn.h index 9b9744f9b4..947b4a5458 100644 --- a/deps/ngtcp2/lib/ngtcp2_conn.h +++ b/deps/ngtcp2/lib/ngtcp2_conn.h @@ -110,6 +110,9 @@ typedef enum { connection ID the remote endpoint provides to store. It must be the power of 2. */ #define NGTCP2_MAX_DCID_POOL_SIZE 16 +/* NGTCP2_MAX_DCID_RETIRED_SIZE is the maximum number of retired DCID + kept to catch in-flight packet on retired path. */ +#define NGTCP2_MAX_DCID_RETIRED_SIZE 2 /* NGTCP2_MIN_SCID_POOL_SIZE is the minimum number of source connection ID the local endpoint provides to the remote endpoint. It must be at least 8 as per the spec. */ @@ -309,6 +312,9 @@ struct ngtcp2_conn { ngtcp2_ringbuf bound; /* unused is a set of unused CID received from peer. */ ngtcp2_ringbuf unused; + /* retired is a set of CID retired by local endpoint. Keep them + in 3*PTO to catch packets in flight along the old path. */ + ngtcp2_ringbuf retired; } dcid; struct { diff --git a/deps/ngtcp2/lib/ngtcp2_gaptr.c b/deps/ngtcp2/lib/ngtcp2_gaptr.c index cfcef78570..cdbecd1df1 100644 --- a/deps/ngtcp2/lib/ngtcp2_gaptr.c +++ b/deps/ngtcp2/lib/ngtcp2_gaptr.c @@ -31,15 +31,19 @@ int ngtcp2_gaptr_init(ngtcp2_gaptr *gaptr, const ngtcp2_mem *mem) { int rv; - ngtcp2_range key = {0, UINT64_MAX}; - rv = ngtcp2_psl_init(&gaptr->gap, mem); + ngtcp2_range range = {0, UINT64_MAX}; + ngtcp2_ksl_key key; + + rv = ngtcp2_ksl_init(&gaptr->gap, ngtcp2_ksl_range_compar, + sizeof(ngtcp2_range), mem); if (rv != 0) { return rv; } - rv = ngtcp2_psl_insert(&gaptr->gap, NULL, &key, NULL); + rv = ngtcp2_ksl_insert(&gaptr->gap, NULL, ngtcp2_ksl_key_ptr(&key, &range), + NULL); if (rv != 0) { - ngtcp2_psl_free(&gaptr->gap); + ngtcp2_ksl_free(&gaptr->gap); return rv; } @@ -53,65 +57,73 @@ void ngtcp2_gaptr_free(ngtcp2_gaptr *gaptr) { return; } - ngtcp2_psl_free(&gaptr->gap); + ngtcp2_ksl_free(&gaptr->gap); } int ngtcp2_gaptr_push(ngtcp2_gaptr *gaptr, uint64_t offset, size_t datalen) { int rv; ngtcp2_range k, m, l, r, q = {offset, offset + datalen}; - ngtcp2_psl_it it; + ngtcp2_ksl_it it; + ngtcp2_ksl_key key, old_key; - it = ngtcp2_psl_lower_bound(&gaptr->gap, &q); + it = ngtcp2_ksl_lower_bound_compar(&gaptr->gap, ngtcp2_ksl_key_ptr(&key, &q), + ngtcp2_ksl_range_exclusive_compar); - for (; !ngtcp2_psl_it_end(&it);) { - k = ngtcp2_psl_it_range(&it); + for (; !ngtcp2_ksl_it_end(&it);) { + k = *(ngtcp2_range *)ngtcp2_ksl_it_key(&it).ptr; m = ngtcp2_range_intersect(&q, &k); if (!ngtcp2_range_len(&m)) { break; } if (ngtcp2_range_eq(&k, &m)) { - rv = ngtcp2_psl_remove(&gaptr->gap, &it, &k); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&gaptr->gap, &it, ngtcp2_ksl_key_ptr(&key, &k)); continue; } ngtcp2_range_cut(&l, &r, &k, &m); if (ngtcp2_range_len(&l)) { - ngtcp2_psl_update_range(&gaptr->gap, &k, &l); + ngtcp2_ksl_update_key(&gaptr->gap, ngtcp2_ksl_key_ptr(&old_key, &k), + ngtcp2_ksl_key_ptr(&key, &l)); if (ngtcp2_range_len(&r)) { - rv = ngtcp2_psl_insert(&gaptr->gap, &it, &r, NULL); + rv = ngtcp2_ksl_insert(&gaptr->gap, &it, ngtcp2_ksl_key_ptr(&key, &r), + NULL); if (rv != 0) { return rv; } } } else if (ngtcp2_range_len(&r)) { - ngtcp2_psl_update_range(&gaptr->gap, &k, &r); + ngtcp2_ksl_update_key(&gaptr->gap, ngtcp2_ksl_key_ptr(&old_key, &k), + ngtcp2_ksl_key_ptr(&key, &r)); } - ngtcp2_psl_it_next(&it); + ngtcp2_ksl_it_next(&it); } return 0; } uint64_t ngtcp2_gaptr_first_gap_offset(ngtcp2_gaptr *gaptr) { - ngtcp2_psl_it it = ngtcp2_psl_begin(&gaptr->gap); - ngtcp2_range r = ngtcp2_psl_it_range(&it); + ngtcp2_ksl_it it = ngtcp2_ksl_begin(&gaptr->gap); + ngtcp2_range r = *(ngtcp2_range *)ngtcp2_ksl_it_key(&it).ptr; return r.begin; } -ngtcp2_psl_it ngtcp2_gaptr_get_first_gap_after(ngtcp2_gaptr *gaptr, +ngtcp2_ksl_it ngtcp2_gaptr_get_first_gap_after(ngtcp2_gaptr *gaptr, uint64_t offset) { ngtcp2_range q = {offset, offset + 1}; - return ngtcp2_psl_lower_bound(&gaptr->gap, &q); + ngtcp2_ksl_key key; + return ngtcp2_ksl_lower_bound_compar(&gaptr->gap, + ngtcp2_ksl_key_ptr(&key, &q), + ngtcp2_ksl_range_exclusive_compar); } int ngtcp2_gaptr_is_pushed(ngtcp2_gaptr *gaptr, uint64_t offset, size_t datalen) { + ngtcp2_ksl_key key; ngtcp2_range q = {offset, offset + datalen}; - ngtcp2_psl_it it = ngtcp2_psl_lower_bound(&gaptr->gap, &q); - ngtcp2_range k = ngtcp2_psl_it_range(&it); + ngtcp2_ksl_it it = + ngtcp2_ksl_lower_bound_compar(&gaptr->gap, ngtcp2_ksl_key_ptr(&key, &q), + ngtcp2_ksl_range_exclusive_compar); + ngtcp2_range k = *(ngtcp2_range *)ngtcp2_ksl_it_key(&it).ptr; ngtcp2_range m = ngtcp2_range_intersect(&q, &k); return ngtcp2_range_len(&m) == 0; } diff --git a/deps/ngtcp2/lib/ngtcp2_gaptr.h b/deps/ngtcp2/lib/ngtcp2_gaptr.h index 7f08ee3fce..1affbb7e8b 100644 --- a/deps/ngtcp2/lib/ngtcp2_gaptr.h +++ b/deps/ngtcp2/lib/ngtcp2_gaptr.h @@ -33,7 +33,7 @@ #include "ngtcp2_mem.h" #include "ngtcp2_range.h" -#include "ngtcp2_psl.h" +#include "ngtcp2_ksl.h" /* * ngtcp2_gaptr maintains the gap in the range [0, UINT64_MAX). @@ -41,7 +41,7 @@ typedef struct { /* gap maintains the range of offset which is not received yet. Initially, its range is [0, UINT64_MAX). */ - ngtcp2_psl gap; + ngtcp2_ksl gap; /* mem is custom memory allocator */ const ngtcp2_mem *mem; } ngtcp2_gaptr; @@ -84,7 +84,7 @@ uint64_t ngtcp2_gaptr_first_gap_offset(ngtcp2_gaptr *gaptr); * ngtcp2_gaptr_get_first_gap_after returns the iterator pointing to * the first gap which overlaps or comes after |offset|. */ -ngtcp2_psl_it ngtcp2_gaptr_get_first_gap_after(ngtcp2_gaptr *gaptr, +ngtcp2_ksl_it ngtcp2_gaptr_get_first_gap_after(ngtcp2_gaptr *gaptr, uint64_t offset); /* diff --git a/deps/ngtcp2/lib/ngtcp2_ksl.c b/deps/ngtcp2/lib/ngtcp2_ksl.c index f4d9d728da..6af35eac67 100644 --- a/deps/ngtcp2/lib/ngtcp2_ksl.c +++ b/deps/ngtcp2/lib/ngtcp2_ksl.c @@ -31,45 +31,87 @@ #include "ngtcp2_macro.h" #include "ngtcp2_mem.h" +#include "ngtcp2_range.h" -int ngtcp2_ksl_init(ngtcp2_ksl *ksl, ngtcp2_ksl_compar compar, - const ngtcp2_ksl_key *inf_key, const ngtcp2_mem *mem) { +static size_t ksl_nodelen(size_t keylen) { + return (sizeof(ngtcp2_ksl_node) + keylen - sizeof(uint64_t) + 0xf) & + (size_t)~0xf; +} + +static size_t ksl_blklen(size_t nodelen) { + return sizeof(ngtcp2_ksl_blk) + nodelen * NGTCP2_KSL_MAX_NBLK - + sizeof(uint64_t); +} + +/* + * ksl_node_set_key sets |key| to |node|. + */ +static void ksl_node_set_key(ngtcp2_ksl *ksl, ngtcp2_ksl_node *node, + const void *key) { + memcpy(&node->key, key, ksl->keylen); +} + +/* + * ksl_node_key assigns the pointer to key of |node| to key->ptr and + * returns |key|. + */ +static ngtcp2_ksl_key *ksl_node_key(ngtcp2_ksl_key *key, + ngtcp2_ksl_node *node) { + key->ptr = &node->key; + return key; +} + +/* + * ksl_nth_node returns |n|th node under |blk|. + */ +static ngtcp2_ksl_node *ksl_nth_node(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, + size_t n) { + return (ngtcp2_ksl_node *)(void *)(blk->nodes + ksl->nodelen * n); +} + +ngtcp2_ksl_node *ngtcp2_ksl_nth_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, + size_t n) { + return ksl_nth_node(ksl, blk, n); +} + +int ngtcp2_ksl_init(ngtcp2_ksl *ksl, ngtcp2_ksl_compar compar, size_t keylen, + const ngtcp2_mem *mem) { + size_t nodelen = ksl_nodelen(keylen); + size_t blklen = ksl_blklen(nodelen); ngtcp2_ksl_blk *head; - ksl->head = ngtcp2_mem_malloc(mem, sizeof(ngtcp2_ksl_blk)); + ksl->head = ngtcp2_mem_malloc(mem, blklen); if (!ksl->head) { return NGTCP2_ERR_NOMEM; } ksl->front = ksl->back = ksl->head; ksl->compar = compar; - ksl->inf_key = *inf_key; + ksl->keylen = keylen; + ksl->nodelen = nodelen; ksl->n = 0; ksl->mem = mem; head = ksl->head; - head->next = head->prev = NULL; - head->n = 1; + head->n = 0; head->leaf = 1; - head->nodes[0].key = ksl->inf_key; - head->nodes[0].data = NULL; return 0; } /* - * free_blk frees |blk| recursively. + * ksl_free_blk frees |blk| recursively. */ -static void free_blk(ngtcp2_ksl_blk *blk, const ngtcp2_mem *mem) { +static void ksl_free_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { size_t i; if (!blk->leaf) { for (i = 0; i < blk->n; ++i) { - free_blk(blk->nodes[i].blk, mem); + ksl_free_blk(ksl, ksl_nth_node(ksl, blk, i)->blk); } } - ngtcp2_mem_free(mem, blk); + ngtcp2_mem_free(ksl->mem, blk); } void ngtcp2_ksl_free(ngtcp2_ksl *ksl) { @@ -77,7 +119,7 @@ void ngtcp2_ksl_free(ngtcp2_ksl *ksl) { return; } - free_blk(ksl->head, ksl->mem); + ksl_free_blk(ksl, ksl->head); } /* @@ -91,7 +133,7 @@ void ngtcp2_ksl_free(ngtcp2_ksl *ksl) { static ngtcp2_ksl_blk *ksl_split_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { ngtcp2_ksl_blk *rblk; - rblk = ngtcp2_mem_malloc(ksl->mem, sizeof(ngtcp2_ksl_blk)); + rblk = ngtcp2_mem_malloc(ksl->mem, ksl_blklen(ksl->nodelen)); if (rblk == NULL) { return NULL; } @@ -108,8 +150,8 @@ static ngtcp2_ksl_blk *ksl_split_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { rblk->n = blk->n / 2; - memcpy(rblk->nodes, &blk->nodes[blk->n - rblk->n], - sizeof(ngtcp2_ksl_node) * rblk->n); + memcpy(rblk->nodes, blk->nodes + ksl->nodelen * (blk->n - rblk->n), + ksl->nodelen * rblk->n); blk->n -= rblk->n; @@ -131,22 +173,25 @@ static ngtcp2_ksl_blk *ksl_split_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { * Out of memory. */ static int ksl_split_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { - ngtcp2_ksl_blk *lblk = blk->nodes[i].blk, *rblk; + ngtcp2_ksl_node *node; + ngtcp2_ksl_blk *lblk = ksl_nth_node(ksl, blk, i)->blk, *rblk; rblk = ksl_split_blk(ksl, lblk); if (rblk == NULL) { return NGTCP2_ERR_NOMEM; } - memmove(&blk->nodes[i + 2], &blk->nodes[i + 1], - sizeof(ngtcp2_ksl_node) * (blk->n - (i + 1))); - - blk->nodes[i + 1].blk = rblk; + memmove(blk->nodes + (i + 2) * ksl->nodelen, + blk->nodes + (i + 1) * ksl->nodelen, + ksl->nodelen * (blk->n - (i + 1))); + node = ksl_nth_node(ksl, blk, i + 1); + node->blk = rblk; ++blk->n; + ksl_node_set_key(ksl, node, &ksl_nth_node(ksl, rblk, rblk->n - 1)->key); - blk->nodes[i].key = lblk->nodes[lblk->n - 1].key; - blk->nodes[i + 1].key = rblk->nodes[rblk->n - 1].key; + node = ksl_nth_node(ksl, blk, i); + ksl_node_set_key(ksl, node, &ksl_nth_node(ksl, lblk, lblk->n - 1)->key); return 0; } @@ -163,6 +208,7 @@ static int ksl_split_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { */ static int ksl_split_head(ngtcp2_ksl *ksl) { ngtcp2_ksl_blk *rblk = NULL, *lblk, *nhead = NULL; + ngtcp2_ksl_node *node; rblk = ksl_split_blk(ksl, ksl->head); if (rblk == NULL) { @@ -171,7 +217,7 @@ static int ksl_split_head(ngtcp2_ksl *ksl) { lblk = ksl->head; - nhead = ngtcp2_mem_malloc(ksl->mem, sizeof(ngtcp2_ksl_blk)); + nhead = ngtcp2_mem_malloc(ksl->mem, ksl_blklen(ksl->nodelen)); if (nhead == NULL) { ngtcp2_mem_free(ksl->mem, rblk); return NGTCP2_ERR_NOMEM; @@ -180,10 +226,13 @@ static int ksl_split_head(ngtcp2_ksl *ksl) { nhead->n = 2; nhead->leaf = 0; - nhead->nodes[0].key = lblk->nodes[lblk->n - 1].key; - nhead->nodes[0].blk = lblk; - nhead->nodes[1].key = rblk->nodes[rblk->n - 1].key; - nhead->nodes[1].blk = rblk; + node = ksl_nth_node(ksl, nhead, 0); + ksl_node_set_key(ksl, node, &ksl_nth_node(ksl, lblk, lblk->n - 1)->key); + node->blk = lblk; + + node = ksl_nth_node(ksl, nhead, 1); + ksl_node_set_key(ksl, node, &ksl_nth_node(ksl, rblk, rblk->n - 1)->key); + node->blk = rblk; ksl->head = nhead; @@ -196,26 +245,46 @@ static int ksl_split_head(ngtcp2_ksl *ksl) { * of nodes contained by |blk| is strictly less than * NGTCP2_KSL_MAX_NBLK. */ -static void insert_node(ngtcp2_ksl_blk *blk, size_t i, - const ngtcp2_ksl_key *key, void *data) { +static void ksl_insert_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i, + const ngtcp2_ksl_key *key, void *data) { ngtcp2_ksl_node *node; assert(blk->n < NGTCP2_KSL_MAX_NBLK); - memmove(&blk->nodes[i + 1], &blk->nodes[i], - sizeof(ngtcp2_ksl_node) * (blk->n - i)); + memmove(blk->nodes + (i + 1) * ksl->nodelen, blk->nodes + i * ksl->nodelen, + ksl->nodelen * (blk->n - i)); - node = &blk->nodes[i]; - node->key = *key; + node = ksl_nth_node(ksl, blk, i); + ksl_node_set_key(ksl, node, key->ptr); node->data = data; ++blk->n; } +static size_t ksl_bsearch(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, + const ngtcp2_ksl_key *key, ngtcp2_ksl_compar compar) { + ssize_t left = -1, right = (ssize_t)blk->n, mid; + ngtcp2_ksl_node *node; + ngtcp2_ksl_key node_key; + + while (right - left > 1) { + mid = (left + right) / 2; + node = ksl_nth_node(ksl, blk, (size_t)mid); + if (compar(ksl_node_key(&node_key, node), key)) { + left = mid; + } else { + right = mid; + } + } + + return (size_t)right; +} + int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, const ngtcp2_ksl_key *key, void *data) { ngtcp2_ksl_blk *blk = ksl->head; ngtcp2_ksl_node *node; + ngtcp2_ksl_key node_key; size_t i; int rv; @@ -228,26 +297,51 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, } for (;;) { - for (i = 0, node = &blk->nodes[i]; ksl->compar(&node->key, key); - ++i, ++node) - ; + i = ksl_bsearch(ksl, blk, key, ksl->compar); if (blk->leaf) { - insert_node(blk, i, key, data); + ksl_insert_node(ksl, blk, i, key, data); ++ksl->n; if (it) { - ngtcp2_ksl_it_init(it, blk, i, ksl->compar, &ksl->inf_key); + ngtcp2_ksl_it_init(it, ksl, blk, i); } return 0; } + if (i == blk->n) { + /* This insertion extends the largest key in this subtree. */ + for (; !blk->leaf;) { + node = ksl_nth_node(ksl, blk, blk->n - 1); + if (node->blk->n == NGTCP2_KSL_MAX_NBLK) { + rv = ksl_split_node(ksl, blk, blk->n - 1); + if (rv != 0) { + return rv; + } + node = ksl_nth_node(ksl, blk, blk->n - 1); + } + ksl_node_set_key(ksl, node, key->ptr); + blk = node->blk; + } + ksl_insert_node(ksl, blk, blk->n, key, data); + ++ksl->n; + if (it) { + ngtcp2_ksl_it_init(it, ksl, blk, blk->n - 1); + } + return 0; + } + + node = ksl_nth_node(ksl, blk, i); + if (node->blk->n == NGTCP2_KSL_MAX_NBLK) { rv = ksl_split_node(ksl, blk, i); if (rv != 0) { return rv; } - if (ksl->compar(&node->key, key)) { - node = &blk->nodes[i + 1]; + if (ksl->compar(ksl_node_key(&node_key, node), key)) { + node = ksl_nth_node(ksl, blk, i + 1); + if (ksl->compar(ksl_node_key(&node_key, node), key)) { + ksl_node_set_key(ksl, node, key->ptr); + } } } @@ -256,11 +350,12 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, } /* - * remove_node removes the node included in |blk| at the index of |i|. + * ksl_remove_node removes the node included in |blk| at the index of + * |i|. */ -static void remove_node(ngtcp2_ksl_blk *blk, size_t i) { - memmove(&blk->nodes[i], &blk->nodes[i + 1], - sizeof(ngtcp2_ksl_node) * (blk->n - (i + 1))); +static void ksl_remove_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { + memmove(blk->nodes + i * ksl->nodelen, blk->nodes + (i + 1) * ksl->nodelen, + ksl->nodelen * (blk->n - (i + 1))); --blk->n; } @@ -281,13 +376,13 @@ static ngtcp2_ksl_blk *ksl_merge_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, assert(i + 1 < blk->n); - lblk = blk->nodes[i].blk; - rblk = blk->nodes[i + 1].blk; + lblk = ksl_nth_node(ksl, blk, i)->blk; + rblk = ksl_nth_node(ksl, blk, i + 1)->blk; assert(lblk->n + rblk->n < NGTCP2_KSL_MAX_NBLK); - memcpy(&lblk->nodes[lblk->n], &rblk->nodes[0], - sizeof(ngtcp2_ksl_node) * rblk->n); + memcpy(lblk->nodes + ksl->nodelen * lblk->n, rblk->nodes, + ksl->nodelen * rblk->n); lblk->n += rblk->n; lblk->next = rblk->next; @@ -303,144 +398,68 @@ static ngtcp2_ksl_blk *ksl_merge_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, ngtcp2_mem_free(ksl->mem, ksl->head); ksl->head = lblk; } else { - remove_node(blk, i + 1); - blk->nodes[i].key = lblk->nodes[lblk->n - 1].key; + ksl_remove_node(ksl, blk, i + 1); + ksl_node_set_key(ksl, ksl_nth_node(ksl, blk, i), + &ksl_nth_node(ksl, lblk, lblk->n - 1)->key); } return lblk; } /* - * ksl_relocate_node replaces the key at the index |*pi| in - * *pblk->nodes with something other without violating contract. It - * might involve merging 2 nodes or moving a node to left or right. - * - * It assigns the index of the block in |*pblk| where the node is - * moved to |*pi|. If merging 2 nodes occurs and it becomes new head, - * the new head is assigned to |*pblk| and it still contains the key. - * The caller should handle this situation. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory. - */ -static int ksl_relocate_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk **pblk, - size_t *pi) { - ngtcp2_ksl_blk *blk = *pblk; - size_t i = *pi; - ngtcp2_ksl_node *node = &blk->nodes[i]; - ngtcp2_ksl_node *rnode = &blk->nodes[i + 1]; - size_t j; - int rv; - - assert(i + 1 < blk->n); - - if (node->blk->n == NGTCP2_KSL_MIN_NBLK && - node->blk->n + rnode->blk->n < NGTCP2_KSL_MAX_NBLK) { - j = node->blk->n - 1; - blk = ksl_merge_node(ksl, blk, i); - if (blk != ksl->head) { - return 0; - } - *pblk = blk; - i = j; - if (blk->leaf) { - *pi = i; - return 0; - } - node = &blk->nodes[i]; - rnode = &blk->nodes[i + 1]; - - if (node->blk->n == NGTCP2_KSL_MIN_NBLK && - node->blk->n + rnode->blk->n < NGTCP2_KSL_MAX_NBLK) { - j = node->blk->n - 1; - blk = ksl_merge_node(ksl, blk, i); - assert(blk != ksl->head); - *pi = j; - return 0; - } - } - - if (node->blk->n < rnode->blk->n) { - node->blk->nodes[node->blk->n] = rnode->blk->nodes[0]; - memmove(&rnode->blk->nodes[0], &rnode->blk->nodes[1], - sizeof(ngtcp2_ksl_node) * (rnode->blk->n - 1)); - --rnode->blk->n; - ++node->blk->n; - node->key = node->blk->nodes[node->blk->n - 1].key; - *pi = i; - return 0; - } - - if (rnode->blk->n == NGTCP2_KSL_MAX_NBLK) { - rv = ksl_split_node(ksl, blk, i + 1); - if (rv != 0) { - return rv; - } - } - - memmove(&rnode->blk->nodes[1], &rnode->blk->nodes[0], - sizeof(ngtcp2_ksl_node) * rnode->blk->n); - - rnode->blk->nodes[0] = node->blk->nodes[node->blk->n - 1]; - ++rnode->blk->n; - - --node->blk->n; - - node->key = node->blk->nodes[node->blk->n - 1].key; - - *pi = i + 1; - return 0; -} - -/* - * shift_left moves the first node in blk->nodes[i]->blk->nodes to + * ksl_shift_left moves the first node in blk->nodes[i]->blk->nodes to * blk->nodes[i - 1]->blk->nodes. */ -static void shift_left(ngtcp2_ksl_blk *blk, size_t i) { - ngtcp2_ksl_node *lnode, *rnode; +static void ksl_shift_left(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { + ngtcp2_ksl_node *lnode, *rnode, *dest, *src; assert(i > 0); - lnode = &blk->nodes[i - 1]; - rnode = &blk->nodes[i]; + lnode = ksl_nth_node(ksl, blk, i - 1); + rnode = ksl_nth_node(ksl, blk, i); assert(lnode->blk->n < NGTCP2_KSL_MAX_NBLK); assert(rnode->blk->n > NGTCP2_KSL_MIN_NBLK); - lnode->blk->nodes[lnode->blk->n] = rnode->blk->nodes[0]; - lnode->key = lnode->blk->nodes[lnode->blk->n].key; + dest = ksl_nth_node(ksl, lnode->blk, lnode->blk->n); + src = ksl_nth_node(ksl, rnode->blk, 0); + + memcpy(dest, src, ksl->nodelen); + ksl_node_set_key(ksl, lnode, &dest->key); ++lnode->blk->n; --rnode->blk->n; - memmove(&rnode->blk->nodes[0], &rnode->blk->nodes[1], - sizeof(ngtcp2_ksl_node) * rnode->blk->n); + memmove(rnode->blk->nodes, rnode->blk->nodes + ksl->nodelen, + ksl->nodelen * rnode->blk->n); } /* - * shift_right moves the last node in blk->nodes[i]->blk->nodes to + * ksl_shift_right moves the last node in blk->nodes[i]->blk->nodes to * blk->nodes[i + 1]->blk->nodes. */ -static void shift_right(ngtcp2_ksl_blk *blk, size_t i) { - ngtcp2_ksl_node *lnode, *rnode; +static void ksl_shift_right(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { + ngtcp2_ksl_node *lnode, *rnode, *dest, *src; assert(i < blk->n - 1); - lnode = &blk->nodes[i]; - rnode = &blk->nodes[i + 1]; + lnode = ksl_nth_node(ksl, blk, i); + rnode = ksl_nth_node(ksl, blk, i + 1); assert(lnode->blk->n > NGTCP2_KSL_MIN_NBLK); assert(rnode->blk->n < NGTCP2_KSL_MAX_NBLK); - memmove(&rnode->blk->nodes[1], &rnode->blk->nodes[0], - sizeof(ngtcp2_ksl_node) * rnode->blk->n); + memmove(rnode->blk->nodes + ksl->nodelen, rnode->blk->nodes, + ksl->nodelen * rnode->blk->n); ++rnode->blk->n; - rnode->blk->nodes[0] = lnode->blk->nodes[lnode->blk->n - 1]; + + dest = ksl_nth_node(ksl, rnode->blk, 0); + src = ksl_nth_node(ksl, lnode->blk, lnode->blk->n - 1); + + memcpy(dest, src, ksl->nodelen); --lnode->blk->n; - lnode->key = lnode->blk->nodes[lnode->blk->n - 1].key; + ksl_node_set_key(ksl, lnode, + &ksl_nth_node(ksl, lnode->blk, lnode->blk->n - 1)->key); } /* @@ -452,100 +471,133 @@ static int key_equal(ngtcp2_ksl_compar compar, const ngtcp2_ksl_key *lhs, return !compar(lhs, rhs) && !compar(rhs, lhs); } -int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, - const ngtcp2_ksl_key *key) { +void ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, + const ngtcp2_ksl_key *key) { ngtcp2_ksl_blk *blk = ksl->head, *lblk, *rblk; ngtcp2_ksl_node *node; - size_t i, j; - int rv; + size_t i; - if (!blk->leaf && blk->n == NGTCP2_KSL_MAX_NBLK) { - rv = ksl_split_head(ksl); - if (rv != 0) { - return rv; - } - blk = ksl->head; + if (!blk->leaf && blk->n == 2 && + ksl_nth_node(ksl, blk, 0)->blk->n == NGTCP2_KSL_MIN_NBLK && + ksl_nth_node(ksl, blk, 1)->blk->n == NGTCP2_KSL_MIN_NBLK) { + blk = ksl_merge_node(ksl, ksl->head, 0); } for (;;) { - for (i = 0, node = &blk->nodes[i]; ksl->compar(&node->key, key); - ++i, ++node) - ; + i = ksl_bsearch(ksl, blk, key, ksl->compar); + + assert(i < blk->n); if (blk->leaf) { assert(i < blk->n); - remove_node(blk, i); + ksl_remove_node(ksl, blk, i); --ksl->n; if (it) { - if (blk->n == i) { - ngtcp2_ksl_it_init(it, blk->next, 0, ksl->compar, &ksl->inf_key); + if (blk->n == i && blk->next) { + ngtcp2_ksl_it_init(it, ksl, blk->next, 0); } else { - ngtcp2_ksl_it_init(it, blk, i, ksl->compar, &ksl->inf_key); + ngtcp2_ksl_it_init(it, ksl, blk, i); } } - return 0; + return; } - if (node->blk->n == NGTCP2_KSL_MAX_NBLK) { - rv = ksl_split_node(ksl, blk, i); - if (rv != 0) { - return rv; - } - if (ksl->compar(&node->key, key)) { - ++i; - node = &blk->nodes[i]; + node = ksl_nth_node(ksl, blk, i); + + if (node->blk->n == NGTCP2_KSL_MIN_NBLK) { + if (i > 0 && (lblk = ksl_nth_node(ksl, blk, i - 1)->blk)->n > + NGTCP2_KSL_MIN_NBLK) { + ksl_shift_right(ksl, blk, i - 1); + } else if (i + 1 < blk->n && + (rblk = ksl_nth_node(ksl, blk, i + 1)->blk)->n > + NGTCP2_KSL_MIN_NBLK) { + ksl_shift_left(ksl, blk, i + 1); + } else if (i > 0) { + assert(lblk); + assert(lblk->n + node->blk->n < NGTCP2_KSL_MAX_NBLK); + blk = ksl_merge_node(ksl, blk, i - 1); + } else { + assert(i + 1 < blk->n); + assert(rblk); + assert(node->blk->n + rblk->n < NGTCP2_KSL_MAX_NBLK); + blk = ksl_merge_node(ksl, blk, i); } + } else { + blk = node->blk; } + } +} - if (key_equal(ksl->compar, &node->key, key)) { - rv = ksl_relocate_node(ksl, &blk, &i); - if (rv != 0) { - return rv; - } - if (!blk->leaf) { - node = &blk->nodes[i]; - blk = node->blk; - } - } else if (node->blk->n == NGTCP2_KSL_MIN_NBLK) { - j = i == 0 ? 0 : i - 1; +ngtcp2_ksl_it ngtcp2_ksl_lower_bound(ngtcp2_ksl *ksl, + const ngtcp2_ksl_key *key) { + ngtcp2_ksl_blk *blk = ksl->head; + ngtcp2_ksl_it it; + size_t i; + + for (;;) { + i = ksl_bsearch(ksl, blk, key, ksl->compar); - lblk = blk->nodes[j].blk; - rblk = blk->nodes[j + 1].blk; + if (blk->leaf) { + if (i == blk->n && blk->next) { + blk = blk->next; + i = 0; + } + ngtcp2_ksl_it_init(&it, ksl, blk, i); + return it; + } - if (lblk->n + rblk->n < NGTCP2_KSL_MAX_NBLK) { - blk = ksl_merge_node(ksl, blk, j); + if (i == blk->n) { + /* This happens if descendant has smaller key. Fast forward to + find last node in this subtree. */ + for (; !blk->leaf; blk = ksl_nth_node(ksl, blk, blk->n - 1)->blk) + ; + if (blk->next) { + blk = blk->next; + i = 0; } else { - if (i == j) { - shift_left(blk, j + 1); - } else { - shift_right(blk, j); - } - blk = node->blk; + i = blk->n; } - } else { - blk = node->blk; + ngtcp2_ksl_it_init(&it, ksl, blk, i); + return it; } + blk = ksl_nth_node(ksl, blk, i)->blk; } } -ngtcp2_ksl_it ngtcp2_ksl_lower_bound(ngtcp2_ksl *ksl, - const ngtcp2_ksl_key *key) { +ngtcp2_ksl_it ngtcp2_ksl_lower_bound_compar(ngtcp2_ksl *ksl, + const ngtcp2_ksl_key *key, + ngtcp2_ksl_compar compar) { ngtcp2_ksl_blk *blk = ksl->head; - ngtcp2_ksl_node *node; + ngtcp2_ksl_it it; size_t i; for (;;) { - for (i = 0, node = &blk->nodes[i]; ksl->compar(&node->key, key); - ++i, node = &blk->nodes[i]) - ; + i = ksl_bsearch(ksl, blk, key, compar); if (blk->leaf) { - ngtcp2_ksl_it it; - ngtcp2_ksl_it_init(&it, blk, i, ksl->compar, &ksl->inf_key); + if (i == blk->n && blk->next) { + blk = blk->next; + i = 0; + } + ngtcp2_ksl_it_init(&it, ksl, blk, i); return it; } - blk = node->blk; + if (i == blk->n) { + /* This happens if descendant has smaller key. Fast forward to + find last node in this subtree. */ + for (; !blk->leaf; blk = ksl_nth_node(ksl, blk, blk->n - 1)->blk) + ; + if (blk->next) { + blk = blk->next; + i = 0; + } else { + i = blk->n; + } + ngtcp2_ksl_it_init(&it, ksl, blk, i); + return it; + } + blk = ksl_nth_node(ksl, blk, i)->blk; } } @@ -553,43 +605,49 @@ void ngtcp2_ksl_update_key(ngtcp2_ksl *ksl, const ngtcp2_ksl_key *old_key, const ngtcp2_ksl_key *new_key) { ngtcp2_ksl_blk *blk = ksl->head; ngtcp2_ksl_node *node; + ngtcp2_ksl_key node_key; size_t i; for (;;) { - for (i = 0, node = &blk->nodes[i]; ksl->compar(&node->key, old_key); - ++i, node = &blk->nodes[i]) - ; + i = ksl_bsearch(ksl, blk, old_key, ksl->compar); + + assert(i < blk->n); + node = ksl_nth_node(ksl, blk, i); if (blk->leaf) { - assert(key_equal(ksl->compar, &node->key, old_key)); - node->key = *new_key; + assert(key_equal(ksl->compar, ksl_node_key(&node_key, node), old_key)); + ksl_node_set_key(ksl, node, new_key->ptr); return; } - if (key_equal(ksl->compar, &node->key, old_key)) { - node->key = *new_key; + ksl_node_key(&node_key, node); + if (key_equal(ksl->compar, &node_key, old_key) || + ksl->compar(&node_key, new_key)) { + ksl_node_set_key(ksl, node, new_key->ptr); } blk = node->blk; } } -static void ksl_print(ngtcp2_ksl *ksl, const ngtcp2_ksl_blk *blk, - size_t level) { +static void ksl_print(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t level) { size_t i; + ngtcp2_ksl_node *node; + ngtcp2_ksl_key node_key; fprintf(stderr, "LV=%zu n=%zu\n", level, blk->n); if (blk->leaf) { for (i = 0; i < blk->n; ++i) { - fprintf(stderr, " %" PRId64, blk->nodes[i].key.i); + node = ksl_nth_node(ksl, blk, i); + fprintf(stderr, " %" PRId64, *ksl_node_key(&node_key, node)->i); } fprintf(stderr, "\n"); return; } for (i = 0; i < blk->n; ++i) { - ksl_print(ksl, blk->nodes[i].blk, level + 1); + ksl_print(ksl, ksl_nth_node(ksl, blk, i)->blk, level + 1); } } @@ -601,7 +659,7 @@ void ngtcp2_ksl_clear(ngtcp2_ksl *ksl) { if (!ksl->head->leaf) { for (i = 0; i < ksl->head->n; ++i) { - free_blk(ksl->head->nodes[i].blk, ksl->mem); + ksl_free_blk(ksl, ksl_nth_node(ksl, ksl->head, i)->blk); } } @@ -611,42 +669,40 @@ void ngtcp2_ksl_clear(ngtcp2_ksl *ksl) { head = ksl->head; head->next = head->prev = NULL; - head->n = 1; + head->n = 0; head->leaf = 1; - head->nodes[0].key = ksl->inf_key; - head->nodes[0].data = NULL; } void ngtcp2_ksl_print(ngtcp2_ksl *ksl) { ksl_print(ksl, ksl->head, 0); } ngtcp2_ksl_it ngtcp2_ksl_begin(const ngtcp2_ksl *ksl) { ngtcp2_ksl_it it; - ngtcp2_ksl_it_init(&it, ksl->front, 0, ksl->compar, &ksl->inf_key); + ngtcp2_ksl_it_init(&it, ksl, ksl->front, 0); return it; } ngtcp2_ksl_it ngtcp2_ksl_end(const ngtcp2_ksl *ksl) { ngtcp2_ksl_it it; - ngtcp2_ksl_it_init(&it, ksl->back, ksl->back->n - 1, ksl->compar, - &ksl->inf_key); + ngtcp2_ksl_it_init(&it, ksl, ksl->back, ksl->back->n); return it; } -void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl_blk *blk, size_t i, - ngtcp2_ksl_compar compar, - const ngtcp2_ksl_key *inf_key) { +void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl *ksl, + ngtcp2_ksl_blk *blk, size_t i) { + it->ksl = ksl; it->blk = blk; it->i = i; - it->compar = compar; - it->inf_key = *inf_key; } void *ngtcp2_ksl_it_get(const ngtcp2_ksl_it *it) { - return it->blk->nodes[it->i].data; + assert(it->i < it->blk->n); + return ksl_nth_node(it->ksl, it->blk, it->i)->data; } void ngtcp2_ksl_it_next(ngtcp2_ksl_it *it) { - if (++it->i == it->blk->n) { + assert(!ngtcp2_ksl_it_end(it)); + + if (++it->i == it->blk->n && it->blk->next) { it->blk = it->blk->next; it->i = 0; } @@ -664,7 +720,7 @@ void ngtcp2_ksl_it_prev(ngtcp2_ksl_it *it) { } int ngtcp2_ksl_it_end(const ngtcp2_ksl_it *it) { - return key_equal(it->compar, &it->blk->nodes[it->i].key, &it->inf_key); + return it->blk->n == it->i && it->blk->next == NULL; } int ngtcp2_ksl_it_begin(const ngtcp2_ksl_it *it) { @@ -672,15 +728,27 @@ int ngtcp2_ksl_it_begin(const ngtcp2_ksl_it *it) { } ngtcp2_ksl_key ngtcp2_ksl_it_key(const ngtcp2_ksl_it *it) { - return it->blk->nodes[it->i].key; -} + ngtcp2_ksl_key node_key; -ngtcp2_ksl_key *ngtcp2_ksl_key_i(ngtcp2_ksl_key *key, int64_t i) { - key->i = i; - return key; + assert(it->i < it->blk->n); + + return *ksl_node_key(&node_key, ksl_nth_node(it->ksl, it->blk, it->i)); } ngtcp2_ksl_key *ngtcp2_ksl_key_ptr(ngtcp2_ksl_key *key, const void *ptr) { key->ptr = ptr; return key; } + +int ngtcp2_ksl_range_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + const ngtcp2_range *a = lhs->ptr, *b = rhs->ptr; + return a->begin < b->begin; +} + +int ngtcp2_ksl_range_exclusive_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + const ngtcp2_range *a = lhs->ptr, *b = rhs->ptr; + return a->begin < b->begin && + !(ngtcp2_max(a->begin, b->begin) < ngtcp2_min(a->end, b->end)); +} diff --git a/deps/ngtcp2/lib/ngtcp2_ksl.h b/deps/ngtcp2/lib/ngtcp2_ksl.h index deede10200..c9ce128a22 100644 --- a/deps/ngtcp2/lib/ngtcp2_ksl.h +++ b/deps/ngtcp2/lib/ngtcp2_ksl.h @@ -49,7 +49,9 @@ * ngtcp2_ksl_key represents key in ngtcp2_ksl. */ typedef union { - int64_t i; + /* i is defined to retrieve int64_t key for convenience. */ + const int64_t *i; + /* ptr points to the key. */ const void *ptr; } ngtcp2_ksl_key; @@ -62,16 +64,22 @@ typedef struct ngtcp2_ksl_blk ngtcp2_ksl_blk; /* * ngtcp2_ksl_node is a node which contains either ngtcp2_ksl_blk or * opaque data. If a node is an internal node, it contains - * ngtcp2_ksl_blk. Otherwise, it has data. The invariant is that the - * key of internal node dictates the maximum key in its descendants, - * and the corresponding leaf node must exist. + * ngtcp2_ksl_blk. Otherwise, it has data. The key is stored at the + * location starting at key. */ struct ngtcp2_ksl_node { - ngtcp2_ksl_key key; union { ngtcp2_ksl_blk *blk; void *data; }; + union { + uint64_t align; + /* key is a buffer to include key associated to this node. + Because the length of key is unknown until ngtcp2_ksl_init is + called, the actual buffer will be allocated after this + field. */ + uint8_t key[1]; + }; }; /* @@ -86,7 +94,15 @@ struct ngtcp2_ksl_blk { size_t n; /* leaf is nonzero if this block contains leaf nodes. */ int leaf; - ngtcp2_ksl_node nodes[NGTCP2_KSL_MAX_NBLK]; + union { + uint64_t align; + /* nodes is a buffer to contain NGTCP2_KSL_MAX_NBLK + ngtcp2_ksl_node objects. Because ngtcp2_ksl_node object is + allocated along with the additional variable length key + storage, the size of buffer is unknown until ngtcp2_ksl_init is + called. */ + uint8_t nodes[1]; + }; }; /* @@ -96,6 +112,9 @@ struct ngtcp2_ksl_blk { typedef int (*ngtcp2_ksl_compar)(const ngtcp2_ksl_key *lhs, const ngtcp2_ksl_key *rhs); +struct ngtcp2_ksl; +typedef struct ngtcp2_ksl ngtcp2_ksl; + struct ngtcp2_ksl_it; typedef struct ngtcp2_ksl_it ngtcp2_ksl_it; @@ -103,15 +122,11 @@ typedef struct ngtcp2_ksl_it ngtcp2_ksl_it; * ngtcp2_ksl_it is a forward iterator to iterate nodes. */ struct ngtcp2_ksl_it { - const ngtcp2_ksl_blk *blk; + const ngtcp2_ksl *ksl; + ngtcp2_ksl_blk *blk; size_t i; - ngtcp2_ksl_compar compar; - ngtcp2_ksl_key inf_key; }; -struct ngtcp2_ksl; -typedef struct ngtcp2_ksl ngtcp2_ksl; - /* * ngtcp2_ksl is a deterministic paged skip list. */ @@ -123,14 +138,18 @@ struct ngtcp2_ksl { /* back points to the last leaf block. */ ngtcp2_ksl_blk *back; ngtcp2_ksl_compar compar; - ngtcp2_ksl_key inf_key; size_t n; + /* keylen is the size of key */ + size_t keylen; + /* nodelen is the actual size of ngtcp2_ksl_node including key + storage. */ + size_t nodelen; const ngtcp2_mem *mem; }; /* * ngtcp2_ksl_init initializes |ksl|. |compar| specifies compare - * function. |inf_key| specifies the "infinite" key. + * function. |keylen| is the length of key. * * It returns 0 if it succeeds, or one of the following negative error * codes: @@ -138,8 +157,8 @@ struct ngtcp2_ksl { * NGTCP2_ERR_NOMEM * Out of memory. */ -int ngtcp2_ksl_init(ngtcp2_ksl *ksl, ngtcp2_ksl_compar compar, - const ngtcp2_ksl_key *inf_key, const ngtcp2_mem *mem); +int ngtcp2_ksl_init(ngtcp2_ksl *ksl, ngtcp2_ksl_compar compar, size_t keylen, + const ngtcp2_mem *mem); /* * ngtcp2_ksl_free frees resources allocated for |ksl|. If |ksl| is @@ -171,15 +190,9 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, * This function assigns the iterator to |*it|, which points to the * node which is located at the right next of the removed node if |it| * is not NULL. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory. */ -int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, - const ngtcp2_ksl_key *key); +void ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, + const ngtcp2_ksl_key *key); /* * ngtcp2_ksl_lower_bound returns the iterator which points to the @@ -191,6 +204,14 @@ int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, ngtcp2_ksl_it ngtcp2_ksl_lower_bound(ngtcp2_ksl *ksl, const ngtcp2_ksl_key *key); +/* + * ngtcp2_ksl_lower_bound_compar works like ngtcp2_ksl_lower_bound, + * but it takes custom function |compar| to do lower bound search. + */ +ngtcp2_ksl_it ngtcp2_ksl_lower_bound_compar(ngtcp2_ksl *ksl, + const ngtcp2_ksl_key *key, + ngtcp2_ksl_compar compar); + /* * ngtcp2_ksl_update_key replaces the key of nodes which has |old_key| * with |new_key|. |new_key| must be strictly greater than the @@ -225,22 +246,29 @@ size_t ngtcp2_ksl_len(ngtcp2_ksl *ksl); void ngtcp2_ksl_clear(ngtcp2_ksl *ksl); /* - * ngtcp2_ksl_print prints its internal state in stderr. This - * function should be used for the debugging purpose only. + * ngtcp2_ksl_nth_node returns the |n|th node under |blk|. This + * function is provided for unit testing. + */ +ngtcp2_ksl_node *ngtcp2_ksl_nth_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, + size_t n); + +/* + * ngtcp2_ksl_print prints its internal state in stderr. It assumes + * that the key is of type int64_t. This function should be used for + * the debugging purpose only. */ void ngtcp2_ksl_print(ngtcp2_ksl *ksl); /* * ngtcp2_ksl_it_init initializes |it|. */ -void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl_blk *blk, size_t i, - ngtcp2_ksl_compar compar, - const ngtcp2_ksl_key *inf_key); +void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl *ksl, + ngtcp2_ksl_blk *blk, size_t i); /* * ngtcp2_ksl_it_get returns the data associated to the node which - * |it| points to. If this function is called when - * ngtcp2_ksl_it_end(it) returns nonzero, it returns NULL. + * |it| points to. It is undefined to call this function when + * ngtcp2_ksl_it_end(it) returns nonzero. */ void *ngtcp2_ksl_it_get(const ngtcp2_ksl_it *it); @@ -273,21 +301,34 @@ int ngtcp2_ksl_it_begin(const ngtcp2_ksl_it *it); /* * ngtcp2_ksl_key returns the key of the node which |it| points to. - * It is OK to call this function when ngtcp2_ksl_it_end(it) returns - * nonzero. In this case, this function returns inf_key. + * It is undefined to call this function when ngtcp2_ksl_it_end(it) + * returns nonzero. */ ngtcp2_ksl_key ngtcp2_ksl_it_key(const ngtcp2_ksl_it *it); -/* - * ngtcp2_ksl_key_i is a convenient function which initializes |key| - * with |i| and returns |key|. - */ -ngtcp2_ksl_key *ngtcp2_ksl_key_i(ngtcp2_ksl_key *key, int64_t i); - /* * ngtcp2_ksl_key_ptr is a convenient function which initializes |key| * with |ptr| and returns |key|. */ ngtcp2_ksl_key *ngtcp2_ksl_key_ptr(ngtcp2_ksl_key *key, const void *ptr); +/* + * ngtcp2_ksl_range_compar is an implementation of ngtcp2_ksl_compar. + * lhs->ptr and rhs->ptr must point to ngtcp2_range object and the + * function returns nonzero if (const ngtcp2_range *)(lhs->ptr)->begin + * < (const ngtcp2_range *)(rhs->ptr)->begin. + */ +int ngtcp2_ksl_range_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs); + +/* + * ngtcp2_ksl_range_exclusive_compar is an implementation of + * ngtcp2_ksl_compar. lhs->ptr and rhs->ptr must point to + * ngtcp2_range object and the function returns nonzero if (const + * ngtcp2_range *)(lhs->ptr)->begin < (const ngtcp2_range + * *)(rhs->ptr)->begin and the 2 ranges do not intersect. + */ +int ngtcp2_ksl_range_exclusive_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs); + #endif /* NGTCP2_KSL_H */ diff --git a/deps/ngtcp2/lib/ngtcp2_pv.h b/deps/ngtcp2/lib/ngtcp2_pv.h index 56ff0c447e..2b92036625 100644 --- a/deps/ngtcp2/lib/ngtcp2_pv.h +++ b/deps/ngtcp2/lib/ngtcp2_pv.h @@ -58,15 +58,16 @@ typedef enum { NGTCP2_PV_FLAG_NONE, /* NGTCP2_PV_FLAG_DONT_CARE indicates that the outcome of the path validation does not matter. */ - NGTCP2_PV_FLAG_DONT_CARE = 0x02, + NGTCP2_PV_FLAG_DONT_CARE = 0x01, /* NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH indicates that DCID should - be retired after path validation finishes regardless of its - result. */ - NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH = 0x04, - /* NGTCP2_PV_FLAG_VERIFY_OLD_PATH_ON_SUCCESS indicates that the path - validation against the old path should be done after successful - path validation. */ - NGTCP2_PV_FLAG_VERIFY_OLD_PATH_ON_SUCCESS = 0x08, + be retired after path validation is aborted or failed. DCID is + not retired if path validation succeeds. */ + NGTCP2_PV_FLAG_RETIRE_DCID_ON_FINISH = 0x02, + /* NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE indicates that fallback DCID + is available in ngtcp2_pv. If path validation fails, fallback to + the fallback DCID. If path validation succeeds, start path + validation against fallback DCID. */ + NGTCP2_PV_FLAG_FALLBACK_ON_FAILURE = 0x04, } ngtcp2_pv_flag; struct ngtcp2_pv; @@ -80,6 +81,9 @@ struct ngtcp2_pv { ngtcp2_log *log; /* dcid is DCID and path this path validation uses. */ ngtcp2_dcid dcid; + /* fallback_dcid is the usually validated DCID and used as a + fallback if this path validation fails. */ + ngtcp2_dcid fallback_dcid; /* ents is the ring buffer of ngtcp2_pv_entry */ ngtcp2_ringbuf ents; /* timeout is the duration within which this path validation should diff --git a/deps/ngtcp2/lib/ngtcp2_rob.c b/deps/ngtcp2/lib/ngtcp2_rob.c index 90179d096e..6c0aca7e90 100644 --- a/deps/ngtcp2/lib/ngtcp2_rob.c +++ b/deps/ngtcp2/lib/ngtcp2_rob.c @@ -68,10 +68,12 @@ void ngtcp2_rob_data_del(ngtcp2_rob_data *d, const ngtcp2_mem *mem) { int ngtcp2_rob_init(ngtcp2_rob *rob, size_t chunk, const ngtcp2_mem *mem) { int rv; ngtcp2_rob_gap *g; + ngtcp2_ksl_key key; - rv = ngtcp2_psl_init(&rob->gappsl, mem); + rv = ngtcp2_ksl_init(&rob->gapksl, ngtcp2_ksl_range_compar, + sizeof(ngtcp2_range), mem); if (rv != 0) { - goto fail_gappsl_psl_init; + goto fail_gapksl_ksl_init; } rv = ngtcp2_rob_gap_new(&g, 0, UINT64_MAX, mem); @@ -79,14 +81,16 @@ int ngtcp2_rob_init(ngtcp2_rob *rob, size_t chunk, const ngtcp2_mem *mem) { goto fail_rob_gap_new; } - rv = ngtcp2_psl_insert(&rob->gappsl, NULL, &g->range, g); + rv = ngtcp2_ksl_insert(&rob->gapksl, NULL, + ngtcp2_ksl_key_ptr(&key, &g->range), g); if (rv != 0) { - goto fail_gappsl_psl_insert; + goto fail_gapksl_ksl_insert; } - rv = ngtcp2_psl_init(&rob->datapsl, mem); + rv = ngtcp2_ksl_init(&rob->dataksl, ngtcp2_ksl_range_compar, + sizeof(ngtcp2_range), mem); if (rv != 0) { - goto fail_datapsl_psl_init; + goto fail_dataksl_ksl_init; } rob->chunk = chunk; @@ -94,35 +98,34 @@ int ngtcp2_rob_init(ngtcp2_rob *rob, size_t chunk, const ngtcp2_mem *mem) { return 0; -fail_datapsl_psl_init: -fail_gappsl_psl_insert: +fail_dataksl_ksl_init: +fail_gapksl_ksl_insert: ngtcp2_rob_gap_del(g, mem); fail_rob_gap_new: - ngtcp2_psl_free(&rob->gappsl); -fail_gappsl_psl_init: + ngtcp2_ksl_free(&rob->gapksl); +fail_gapksl_ksl_init: return rv; } void ngtcp2_rob_free(ngtcp2_rob *rob) { - static const ngtcp2_range r = {0, 0}; - ngtcp2_psl_it it; + ngtcp2_ksl_it it; if (rob == NULL) { return; } - for (it = ngtcp2_psl_lower_bound(&rob->datapsl, &r); !ngtcp2_psl_it_end(&it); - ngtcp2_psl_it_next(&it)) { - ngtcp2_rob_data_del(ngtcp2_psl_it_get(&it), rob->mem); + for (it = ngtcp2_ksl_begin(&rob->dataksl); !ngtcp2_ksl_it_end(&it); + ngtcp2_ksl_it_next(&it)) { + ngtcp2_rob_data_del(ngtcp2_ksl_it_get(&it), rob->mem); } - for (it = ngtcp2_psl_lower_bound(&rob->gappsl, &r); !ngtcp2_psl_it_end(&it); - ngtcp2_psl_it_next(&it)) { - ngtcp2_rob_gap_del(ngtcp2_psl_it_get(&it), rob->mem); + for (it = ngtcp2_ksl_begin(&rob->gapksl); !ngtcp2_ksl_it_end(&it); + ngtcp2_ksl_it_next(&it)) { + ngtcp2_rob_gap_del(ngtcp2_ksl_it_get(&it), rob->mem); } - ngtcp2_psl_free(&rob->datapsl); - ngtcp2_psl_free(&rob->gappsl); + ngtcp2_ksl_free(&rob->dataksl); + ngtcp2_ksl_free(&rob->gapksl); } static int rob_write_data(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, @@ -131,11 +134,18 @@ static int rob_write_data(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, int rv; ngtcp2_rob_data *d; ngtcp2_range range = {offset, offset + len}; - ngtcp2_psl_it it; - - for (it = ngtcp2_psl_lower_bound(&rob->datapsl, &range); len; - ngtcp2_psl_it_next(&it)) { - d = ngtcp2_psl_it_get(&it); + ngtcp2_ksl_it it; + ngtcp2_ksl_key key; + + for (it = ngtcp2_ksl_lower_bound_compar(&rob->dataksl, + ngtcp2_ksl_key_ptr(&key, &range), + ngtcp2_ksl_range_exclusive_compar); + len; ngtcp2_ksl_it_next(&it)) { + if (ngtcp2_ksl_it_end(&it)) { + d = NULL; + } else { + d = ngtcp2_ksl_it_get(&it); + } if (d == NULL || offset < d->range.begin) { rv = ngtcp2_rob_data_new(&d, (offset / rob->chunk) * rob->chunk, @@ -144,7 +154,8 @@ static int rob_write_data(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, return rv; } - rv = ngtcp2_psl_insert(&rob->datapsl, &it, &d->range, d); + rv = ngtcp2_ksl_insert(&rob->dataksl, &it, + ngtcp2_ksl_key_ptr(&key, &d->range), d); if (rv != 0) { ngtcp2_rob_data_del(d, rob->mem); return rv; @@ -167,22 +178,21 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, int rv; ngtcp2_rob_gap *g; ngtcp2_range m, l, r, q = {offset, offset + datalen}; - ngtcp2_psl_it it; + ngtcp2_ksl_it it; + ngtcp2_ksl_key key, old_key; - it = ngtcp2_psl_lower_bound(&rob->gappsl, &q); + it = ngtcp2_ksl_lower_bound_compar(&rob->gapksl, ngtcp2_ksl_key_ptr(&key, &q), + ngtcp2_ksl_range_exclusive_compar); - for (; !ngtcp2_psl_it_end(&it);) { - g = ngtcp2_psl_it_get(&it); + for (; !ngtcp2_ksl_it_end(&it);) { + g = ngtcp2_ksl_it_get(&it); m = ngtcp2_range_intersect(&q, &g->range); if (!ngtcp2_range_len(&m)) { break; } if (ngtcp2_range_eq(&g->range, &m)) { - rv = ngtcp2_psl_remove(&rob->gappsl, &it, &g->range); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rob->gapksl, &it, ngtcp2_ksl_key_ptr(&key, &g->range)); ngtcp2_rob_gap_del(g, rob->mem); rv = rob_write_data(rob, m.begin, data + (m.begin - offset), ngtcp2_range_len(&m)); @@ -194,7 +204,9 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, } ngtcp2_range_cut(&l, &r, &g->range, &m); if (ngtcp2_range_len(&l)) { - ngtcp2_psl_update_range(&rob->gappsl, &g->range, &l); + ngtcp2_ksl_update_key(&rob->gapksl, + ngtcp2_ksl_key_ptr(&old_key, &g->range), + ngtcp2_ksl_key_ptr(&key, &l)); g->range = l; if (ngtcp2_range_len(&r)) { @@ -203,14 +215,17 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, if (rv != 0) { return rv; } - rv = ngtcp2_psl_insert(&rob->gappsl, &it, &ng->range, ng); + rv = ngtcp2_ksl_insert(&rob->gapksl, &it, + ngtcp2_ksl_key_ptr(&key, &ng->range), ng); if (rv != 0) { ngtcp2_rob_gap_del(ng, rob->mem); return rv; } } } else if (ngtcp2_range_len(&r)) { - ngtcp2_psl_update_range(&rob->gappsl, &g->range, &r); + ngtcp2_ksl_update_key(&rob->gapksl, + ngtcp2_ksl_key_ptr(&old_key, &g->range), + ngtcp2_ksl_key_ptr(&key, &r)); g->range = r; } rv = rob_write_data(rob, m.begin, data + (m.begin - offset), @@ -218,7 +233,7 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, if (rv != 0) { return rv; } - ngtcp2_psl_it_next(&it); + ngtcp2_ksl_it_next(&it); } return 0; } @@ -226,40 +241,36 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, int ngtcp2_rob_remove_prefix(ngtcp2_rob *rob, uint64_t offset) { ngtcp2_rob_gap *g; ngtcp2_rob_data *d; - ngtcp2_psl_it it; - int rv; + ngtcp2_ksl_it it; + ngtcp2_ksl_key key, old_key; - it = ngtcp2_psl_begin(&rob->gappsl); + it = ngtcp2_ksl_begin(&rob->gapksl); - for (; !ngtcp2_psl_it_end(&it);) { - g = ngtcp2_psl_it_get(&it); + for (; !ngtcp2_ksl_it_end(&it);) { + g = ngtcp2_ksl_it_get(&it); if (offset <= g->range.begin) { break; } if (offset < g->range.end) { ngtcp2_range r = {offset, g->range.end}; - ngtcp2_psl_update_range(&rob->gappsl, &g->range, &r); + ngtcp2_ksl_update_key(&rob->gapksl, + ngtcp2_ksl_key_ptr(&old_key, &g->range), + ngtcp2_ksl_key_ptr(&key, &r)); g->range.begin = offset; break; } - rv = ngtcp2_psl_remove(&rob->gappsl, &it, &g->range); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rob->gapksl, &it, ngtcp2_ksl_key_ptr(&key, &g->range)); ngtcp2_rob_gap_del(g, rob->mem); } - it = ngtcp2_psl_begin(&rob->datapsl); + it = ngtcp2_ksl_begin(&rob->dataksl); - for (; !ngtcp2_psl_it_end(&it);) { - d = ngtcp2_psl_it_get(&it); + for (; !ngtcp2_ksl_it_end(&it);) { + d = ngtcp2_ksl_it_get(&it); if (offset < d->range.begin + rob->chunk) { return 0; } - rv = ngtcp2_psl_remove(&rob->datapsl, &it, &d->range); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rob->dataksl, &it, ngtcp2_ksl_key_ptr(&key, &d->range)); ngtcp2_rob_data_del(d, rob->mem); } @@ -270,21 +281,21 @@ size_t ngtcp2_rob_data_at(ngtcp2_rob *rob, const uint8_t **pdest, uint64_t offset) { ngtcp2_rob_gap *g; ngtcp2_rob_data *d; - ngtcp2_psl_it it; + ngtcp2_ksl_it it; - it = ngtcp2_psl_begin(&rob->gappsl); - if (ngtcp2_psl_it_end(&it)) { + it = ngtcp2_ksl_begin(&rob->gapksl); + if (ngtcp2_ksl_it_end(&it)) { return 0; } - g = ngtcp2_psl_it_get(&it); + g = ngtcp2_ksl_it_get(&it); if (g->range.begin <= offset) { return 0; } - it = ngtcp2_psl_begin(&rob->datapsl); - d = ngtcp2_psl_it_get(&it); + it = ngtcp2_ksl_begin(&rob->dataksl); + d = ngtcp2_ksl_it_get(&it); assert(d); assert(d->range.begin <= offset); @@ -295,38 +306,33 @@ size_t ngtcp2_rob_data_at(ngtcp2_rob *rob, const uint8_t **pdest, return ngtcp2_min(g->range.begin, d->range.begin + rob->chunk) - offset; } -int ngtcp2_rob_pop(ngtcp2_rob *rob, uint64_t offset, size_t len) { - ngtcp2_psl_it it; +void ngtcp2_rob_pop(ngtcp2_rob *rob, uint64_t offset, size_t len) { + ngtcp2_ksl_it it; + ngtcp2_ksl_key key; ngtcp2_rob_data *d; - int rv; - it = ngtcp2_psl_begin(&rob->datapsl); - d = ngtcp2_psl_it_get(&it); + it = ngtcp2_ksl_begin(&rob->dataksl); + d = ngtcp2_ksl_it_get(&it); assert(d); if (offset + len < d->range.begin + rob->chunk) { - return 0; + return; } - rv = ngtcp2_psl_remove(&rob->datapsl, NULL, &d->range); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rob->dataksl, NULL, ngtcp2_ksl_key_ptr(&key, &d->range)); ngtcp2_rob_data_del(d, rob->mem); - - return 0; } uint64_t ngtcp2_rob_first_gap_offset(ngtcp2_rob *rob) { - ngtcp2_psl_it it = ngtcp2_psl_begin(&rob->gappsl); + ngtcp2_ksl_it it = ngtcp2_ksl_begin(&rob->gapksl); ngtcp2_rob_gap *g; - if (ngtcp2_psl_it_end(&it)) { + if (ngtcp2_ksl_it_end(&it)) { return UINT64_MAX; } - g = ngtcp2_psl_it_get(&it); + g = ngtcp2_ksl_it_get(&it); return g->range.begin; } diff --git a/deps/ngtcp2/lib/ngtcp2_rob.h b/deps/ngtcp2/lib/ngtcp2_rob.h index 78cf43ca3a..28ae1da6b7 100644 --- a/deps/ngtcp2/lib/ngtcp2_rob.h +++ b/deps/ngtcp2/lib/ngtcp2_rob.h @@ -33,7 +33,7 @@ #include "ngtcp2_mem.h" #include "ngtcp2_range.h" -#include "ngtcp2_psl.h" +#include "ngtcp2_ksl.h" struct ngtcp2_rob_gap; typedef struct ngtcp2_rob_gap ngtcp2_rob_gap; @@ -113,12 +113,12 @@ void ngtcp2_rob_data_del(ngtcp2_rob_data *d, const ngtcp2_mem *mem); * received in out of order. */ typedef struct { - /* gappsl maintains the range of offset which is not received + /* gapksl maintains the range of offset which is not received yet. Initially, its range is [0, UINT64_MAX). */ - ngtcp2_psl gappsl; - /* datapsl maintains the list of buffers which store received data + ngtcp2_ksl gapksl; + /* dataksl maintains the list of buffers which store received data ordered by stream offset. */ - ngtcp2_psl datapsl; + ngtcp2_ksl dataksl; /* mem is custom memory allocator */ const ngtcp2_mem *mem; /* chunk is the size of each buffer in data field */ @@ -186,14 +186,8 @@ size_t ngtcp2_rob_data_at(ngtcp2_rob *rob, const uint8_t **pdest, * * Caller should call this function from offset 0 in non-decreasing * order. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory */ -int ngtcp2_rob_pop(ngtcp2_rob *rob, uint64_t offset, size_t len); +void ngtcp2_rob_pop(ngtcp2_rob *rob, uint64_t offset, size_t len); /* * ngtcp2_rob_first_gap_offset returns the offset to the first gap. diff --git a/deps/ngtcp2/lib/ngtcp2_rtb.c b/deps/ngtcp2/lib/ngtcp2_rtb.c index bcc1f4167f..29d7fcff5a 100644 --- a/deps/ngtcp2/lib/ngtcp2_rtb.c +++ b/deps/ngtcp2/lib/ngtcp2_rtb.c @@ -154,15 +154,13 @@ void ngtcp2_rtb_entry_del(ngtcp2_rtb_entry *ent, const ngtcp2_mem *mem) { } static int greater(const ngtcp2_ksl_key *lhs, const ngtcp2_ksl_key *rhs) { - return lhs->i > rhs->i; + return *lhs->i > *rhs->i; } void ngtcp2_rtb_init(ngtcp2_rtb *rtb, ngtcp2_crypto_level crypto_level, ngtcp2_strm *crypto, ngtcp2_default_cc *cc, ngtcp2_log *log, const ngtcp2_mem *mem) { - ngtcp2_ksl_key inf_key = {-1}; - - ngtcp2_ksl_init(&rtb->ents, greater, &inf_key, mem); + ngtcp2_ksl_init(&rtb->ents, greater, sizeof(int64_t), mem); rtb->crypto = crypto; rtb->cc = cc; rtb->log = log; @@ -234,11 +232,12 @@ static void rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, int ngtcp2_rtb_add(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent) { int rv; + ngtcp2_ksl_key key; ent->next = NULL; rv = ngtcp2_ksl_insert(&rtb->ents, NULL, - (const ngtcp2_ksl_key *)&ent->hd.pkt_num, ent); + ngtcp2_ksl_key_ptr(&key, &ent->hd.pkt_num), ent); if (rv != 0) { return rv; } @@ -252,18 +251,13 @@ ngtcp2_ksl_it ngtcp2_rtb_head(ngtcp2_rtb *rtb) { return ngtcp2_ksl_begin(&rtb->ents); } -static int rtb_remove(ngtcp2_rtb *rtb, ngtcp2_ksl_it *it, - ngtcp2_rtb_entry *ent) { - int rv; +static void rtb_remove(ngtcp2_rtb *rtb, ngtcp2_ksl_it *it, + ngtcp2_rtb_entry *ent) { + ngtcp2_ksl_key key; - rv = ngtcp2_ksl_remove(&rtb->ents, it, - (const ngtcp2_ksl_key *)&ent->hd.pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rtb->ents, it, ngtcp2_ksl_key_ptr(&key, &ent->hd.pkt_num)); rtb_on_remove(rtb, ent); ngtcp2_rtb_entry_del(ent, rtb->mem); - return 0; } static int rtb_call_acked_stream_offset(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent, @@ -376,8 +370,8 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, ngtcp2_max(rtb->largest_acked_tx_pkt_num, largest_ack); /* Assume that ngtcp2_pkt_validate_ack(fr) returns 0 */ - it = ngtcp2_ksl_lower_bound(&rtb->ents, (const ngtcp2_ksl_key *)&largest_ack); - + it = ngtcp2_ksl_lower_bound(&rtb->ents, + ngtcp2_ksl_key_ptr(&key, &largest_ack)); if (ngtcp2_ksl_it_end(&it)) { return 0; } @@ -386,14 +380,14 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, for (; !ngtcp2_ksl_it_end(&it);) { key = ngtcp2_ksl_it_key(&it); - if (min_ack <= key.i && key.i <= largest_ack) { + if (min_ack <= *key.i && *key.i <= largest_ack) { ent = ngtcp2_ksl_it_get(&it); if (conn) { rv = rtb_call_acked_stream_offset(rtb, ent, conn); if (rv != 0) { return rv; } - if (largest_ack == key.i) { + if (largest_ack == *key.i) { largest_pkt_sent_ts = ent->ts; largest_pkt_acked = 1; } @@ -407,10 +401,7 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, /* At this point, it is invalided because rtb->ents might be modified. */ } - rv = rtb_remove(rtb, &it, ent); - if (rv != 0) { - return rv; - } + rtb_remove(rtb, &it, ent); ++num_acked; continue; } @@ -422,14 +413,14 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, min_ack = largest_ack - (int64_t)fr->blks[i].blklen; it = ngtcp2_ksl_lower_bound(&rtb->ents, - (const ngtcp2_ksl_key *)&largest_ack); + ngtcp2_ksl_key_ptr(&key, &largest_ack)); if (ngtcp2_ksl_it_end(&it)) { break; } for (; !ngtcp2_ksl_it_end(&it);) { key = ngtcp2_ksl_it_key(&it); - if (key.i < min_ack) { + if (*key.i < min_ack) { break; } ent = ngtcp2_ksl_it_get(&it); @@ -446,10 +437,7 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, } rtb_on_pkt_acked(rtb, ent); } - rv = rtb_remove(rtb, &it, ent); - if (rv != 0) { - return rv; - } + rtb_remove(rtb, &it, ent); ++num_acked; } @@ -486,17 +474,17 @@ static ngtcp2_duration compute_pkt_loss_delay(const ngtcp2_rcvry_stat *rcs) { return ngtcp2_max(loss_delay, NGTCP2_GRANULARITY); } -int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, - ngtcp2_rcvry_stat *rcs, ngtcp2_duration pto, - ngtcp2_tstamp ts) { +void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, + ngtcp2_rcvry_stat *rcs, ngtcp2_duration pto, + ngtcp2_tstamp ts) { ngtcp2_rtb_entry *ent; ngtcp2_duration loss_delay; ngtcp2_tstamp lost_send_time; ngtcp2_ksl_it it; int64_t lost_pkt_num; - int rv; ngtcp2_tstamp latest_ts, oldest_ts; int64_t last_lost_pkt_num; + ngtcp2_ksl_key key; rtb->loss_time = 0; loss_delay = compute_pkt_loss_delay(rcs); @@ -504,7 +492,7 @@ int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, lost_pkt_num = rtb->largest_acked_tx_pkt_num - NGTCP2_PACKET_THRESHOLD; it = ngtcp2_ksl_lower_bound( - &rtb->ents, (const ngtcp2_ksl_key *)&rtb->largest_acked_tx_pkt_num); + &rtb->ents, ngtcp2_ksl_key_ptr(&key, &rtb->largest_acked_tx_pkt_num)); for (; !ngtcp2_ksl_it_end(&it); ngtcp2_ksl_it_next(&it)) { ent = ngtcp2_ksl_it_get(&it); @@ -515,11 +503,8 @@ int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, for (; !ngtcp2_ksl_it_end(&it);) { ent = ngtcp2_ksl_it_get(&it); - rv = ngtcp2_ksl_remove(&rtb->ents, &it, - (const ngtcp2_ksl_key *)&ent->hd.pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rtb->ents, &it, + ngtcp2_ksl_key_ptr(&key, &ent->hd.pkt_num)); if (last_lost_pkt_num == ent->hd.pkt_num + 1) { last_lost_pkt_num = ent->hd.pkt_num; @@ -539,17 +524,15 @@ int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, rtb->cc, latest_ts - oldest_ts, pto); } - return 0; + return; } } - - return 0; } -int ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { +void ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { ngtcp2_rtb_entry *ent; ngtcp2_ksl_it it; - int rv; + ngtcp2_ksl_key key; it = ngtcp2_ksl_begin(&rtb->ents); @@ -561,11 +544,8 @@ int ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { ngtcp2_log_pkt_lost(rtb->log, &ent->hd, ent->ts); rtb_on_remove(rtb, ent); - rv = ngtcp2_ksl_remove(&rtb->ents, &it, - (const ngtcp2_ksl_key *)&ent->hd.pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rtb->ents, &it, + ngtcp2_ksl_key_ptr(&key, &ent->hd.pkt_num)); if (!(ent->flags & NGTCP2_RTB_FLAG_CRYPTO_TIMEOUT_RETRANSMITTED)) { frame_chain_insert(pfrc, ent->frc); @@ -578,8 +558,6 @@ int ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { ngtcp2_rtb_entry_del(ent, rtb->mem); } - - return 0; } int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { @@ -587,11 +565,12 @@ int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { ngtcp2_ksl_it it; ngtcp2_crypto_frame_chain *nfrc; ngtcp2_frame_chain *frc; - ngtcp2_psl_it gapit; + ngtcp2_ksl_it gapit; ngtcp2_range gap, range; ngtcp2_crypto *fr; int all_acked; int rv; + ngtcp2_ksl_key key; it = ngtcp2_ksl_begin(&rtb->ents); @@ -618,7 +597,7 @@ int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { been acknowledged */ gapit = ngtcp2_gaptr_get_first_gap_after(&rtb->crypto->tx.acked_offset, fr->offset); - gap = ngtcp2_psl_it_range(&gapit); + gap = *(ngtcp2_range *)ngtcp2_ksl_it_key(&gapit).ptr; range.begin = fr->offset; range.end = fr->offset + ngtcp2_vec_len(fr->data, fr->datacnt); @@ -642,11 +621,8 @@ int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc) { /* If the frames that ent contains have been acknowledged, remove it from rtb. Otherwise crypto timer keeps firing. */ rtb_on_remove(rtb, ent); - rv = ngtcp2_ksl_remove(&rtb->ents, &it, - (const ngtcp2_ksl_key *)&ent->hd.pkt_num); - if (rv != 0) { - return rv; - } + ngtcp2_ksl_remove(&rtb->ents, &it, + ngtcp2_ksl_key_ptr(&key, &ent->hd.pkt_num)); ngtcp2_rtb_entry_del(ent, rtb->mem); continue; } diff --git a/deps/ngtcp2/lib/ngtcp2_rtb.h b/deps/ngtcp2/lib/ngtcp2_rtb.h index 7eb5a4dddd..c78f24a385 100644 --- a/deps/ngtcp2/lib/ngtcp2_rtb.h +++ b/deps/ngtcp2/lib/ngtcp2_rtb.h @@ -310,29 +310,17 @@ ssize_t ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, * frames contained them to |*pfrc|. Even when this function fails, * some frames might be prepended to |*pfrc| and the caller should * handle them. |pto| is PTO. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory */ -int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, - ngtcp2_rcvry_stat *rcs, ngtcp2_duration pto, - ngtcp2_tstamp ts); +void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc, + ngtcp2_rcvry_stat *rcs, ngtcp2_duration pto, + ngtcp2_tstamp ts); /* * ngtcp2_rtb_remove_all removes all packets from |rtb| and prepends * all frames to |*pfrc|. Even when this function fails, some frames * might be prepended to |*pfrc| and the caller should handle them. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_NOMEM - * Out of memory */ -int ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc); +void ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc); /* * ngtcp2_rtb_on_crypto_timeout copies all unacknowledged CRYPTO diff --git a/doc/api/errors.md b/doc/api/errors.md index 43b1d99e67..667a40276a 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1571,6 +1571,11 @@ compiled with ICU support. A given value is out of the accepted range. + +### ERR_QUIC_ERROR + +TBD + ### ERR_QUICCLIENTSESSION_FAILED diff --git a/doc/api/quic.md b/doc/api/quic.md index 4b286f40b1..ed5df95e76 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -17,17 +17,14 @@ const quic = require('quic'); const key = getTLSKeySomehow(); const cert = getTLSCertSomehow(); -const ca = getTLSCAListSomehow(); -// The default export of the quic module is the -// createSocket function. -const createSocket = require('quic'); +const { createSocket } = require('quic'); // Create the local QUIC UDP socket... const socket = createSocket({ type: 'udp4', port: 1234 }); // Tell the socket to operate as a server... -socket.listen({ key, cert, ca }); +socket.listen({ key, cert }); socket.on('session', (session) => { // A new server side session has been created! @@ -62,28 +59,40 @@ socket.on('listening', () => { }); ``` -### quic.createSocket([options]) +## quic.createSocket([options]) * `options` {Object} - * `address` {string} The local address to bind to. - * `ipv6Only` {boolean} - * `lookup` {Function} + * `address` {string} The local address to bind to. This may be an IPv4 or IPv6 + address or a hostname. If a hostname is given, it will be resolved to an IP + address. + * `client` {Object} A default configuration for QUIC client sessions created + using `quicsocket.connect()`. + * `lookup` {Function} A custom DNS lookup function. Default `dns.lookup()`. + * `maxConnectionsPerHost` {number} The maximum number of inbound connections + per remote host. Default: `100`. * `port` {number} The local port to bind to. - * `resuseAddr` {boolean} + * `retryTokenTimeout` {number} The maximum number of seconds for retry token + validation. Defaults: `10`. + * `server` {Object} A default configuration for QUIC server sessions. * `type` {string} Either `'udp4'` or `'upd6'` to use either IPv4 or IPv6, respectively. Creates a new `QuicSocket` instance. -### Class: QuicSession +## Class: QuicSession exends EventEmitter * Extends: {EventEmitter} +The `QuicSession` is an abstract base class that defines events, methods, and +properties that are shared by both `QuicClientSession` and `QuicServerSession`. + +Users will not create instances of `QuicSession` directly. + ### Event: `'close'` -Emitted after the `'close'` event if the `QuicSession` was destroyed with +Emitted before the `'close'` event if the `QuicSession` was destroyed with an error. +### Event: `'extendMaxBidiStreams'` + + +Emitted when the maximum number of bidirectional streams has been extended. + +The callback will be invoked with a single argument: + +* `maxStreams` {number} The new maximum number of bidirectional streams + +### Event: `'extendMaxUniStreams'` + + +Emitted when the maximum number of unidirectional streams has been extended. + +The callback will be invoked with a single argument: + +* `maxStreams` {number} The new maximum number of unidirectional streams + ### Event: `'secure'` +* Type: {object} + * `name` {string} The cipher algorithm name. + * `type` {string} The TLS version (currently always `'TLSv1.3'`). + +Information about the cipher algorithm selected for the session. + +### quicsession.close([code[, callback]]) + + +* `code` {number} The error code to when closing the session. Default: `0`. * `callback` {Function} Callback invoked when the close operation is completed -Closes the `QuicSession`. +Begins a graceful close of the `QuicSession`. Existing `QuicStream` instances will be +permitted to close naturally. New `QuicStream` instances will not be permitted. Once +all `QuicStream` instances have closed, the `QuicSession` instance will be destroyed. + +### quicsession.closing + + +* Type: {boolean} + +Set the `true` if the `QuicSession` is in the process of a graceful shutdown. ### quicsession.destroy([error]) +* Returns: {Object} A [Certificate Object][]. + +Returns an object representing the local certificate. The returned object has some +properties corresponding to the fields of the certificate. + +If there is no local certificate, or if the `QuicSession` has been destroyed, an empty +object will be returned. + ### quicsession.getPeerCertificate([detailed]) -* `detailed` {boolean} Defaults to `false` +* `detailed` {boolean} Include the full certificate chain if `true`, otherwise include + just the peer's certificate. +* Returns: {Object} A [Certificate Object][]. + +Returns an object representing the peer's certificate. If the peer does not provide a +certificate, or if the `QuicSession` has been destroyed, an empty object will be returned. + +If the full certificate chain was requested, each certificate will include an `issuerCertificate` +property containing an object representing its issuer's certificate. + +### quicsession.handshakeComplete + + +* Type: {boolean} + +True if the TLS handshake has completed. ### quicsession.openStream([options]) * Extends: {QuicSession} -TBD +The `QuicClientSession` class implements the client side of a QUIC connection. +Instances are created using the `quicsocket.connect()` method. -#### Event: `'sessionTicket'` +### Event: `'sessionTicket'` The `'sessionTicket'` event is emitted when a new TLS session ticket has been generated for the current `QuicClientSession`. The callback is invoked with @@ -224,40 +312,96 @@ three arguments: The `sessionTicket` and `remoteTransportParams` are useful when creating a new `QuicClientSession` to more quickly resume an existing session. -#### quicclientsession.ephemeralKeyInfo +### quicclientsession.ephemeralKeyInfo + + +* Type: {Object} + +An object representing the type, name, and size of parameter of an ephemeral +key exchange in Perfect Forward Secrecy on a client connection. It is an +empty object when the key exchange is not ephemeral. The supported types are +`'DH'` and `'ECDH'`. The `name` property is available only when type is `'ECDH'`. + +For example: `{ type: 'ECDH', name: 'prime256v1', size: 256 }`. + +### quicclientsession.ready -### Class: QuicServerSession +* Type: {boolean} + +True if the `QuicClientSession` is ready for use. False if the `QuicSocket` has not +yet been bound. + +### quicclientsession.readyToMigrate + + +* Type: {boolean} + +Once established, a `QuicClientSession` can be migrated from one `QuicSocket` instance +to another, without requiring the TLS handshake to be reestablished. Migration, however, +can only occur once the TLS handshake is complete and the underlying session has had an +opportunity to generate a pool of extra connection identifiers. + +### quicclientsession.setSocket(socket, callback]) + + +* `socket` {QuicSocket} A `QuicSocket` instance to move this session to. +* `callback` {Function} A callback function that will be invoked once the migration to + the new `QuicSocket` is complete. + +Migrates the `QuicClientSession` to the given `QuicSocket` instance. If the new `QuicSocket` +has not yet been bound to a local UDP port, it will be bound prior to attempting the +migration. If `quicclientsession.readyToMigrate` is `false`, an error will be thrown. + +## Class: QuicServerSession extends QuicSession * Extends: {QuicSession} -TBD +The `QuicServerSession` class implements the server side of a QUIC connection. +Instances are created internally and are emitted using the `QuicSocket` `'session'` +event. -### Class: QuicSocket +## Class: QuicSocket +New instances of `QuicSocket` are created using the `quic.createSocket()` method. + +Once created, a `QuicSocket` can be configured to work as both a client and a server. + ### Event: `'close'` +Emitted after the `QuicSocket` has been destroyed and is no longer usable. + ### Event: `'error'` +Emitted before the `'close'` event if the `QuicSocket` was destroyed with an `error`. + ### Event: `'ready'` +Emitted once the `QuicSocket` has been bound to a local UDP port. + ### Event: `'session'` * `options` {Object} + * `alpn` {string} An ALPN protocol identifier. * `ca` {string|string[]|Buffer|Buffer[]} Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla. Mozilla's CAs are completely replaced when CAs are explicitly specified @@ -492,6 +678,7 @@ added: REPLACEME preferences instead of the client's. When `true`, causes `SSL_OP_CIPHER_SERVER_PREFERENCE` to be set in `secureOptions`, see [OpenSSL Options][] for more information. + * `idleTimeout` {number} * `key` {string|string[]|Buffer|Buffer[]|Object[]} Private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with `options.passphrase`. Multiple keys using different @@ -500,6 +687,16 @@ added: REPLACEME passphrase: ]}`. The object form can only occur in an array. `object.passphrase` is optional. Encrypted keys will be decrypted with `object.passphrase` if provided, or `options.passphrase` if it is not. + * `maxAckDelay` {number} + * `maxCidLen` {number} + * `maxData` {number} + * `maxPacketSize` {number} + * `maxStreamsBidi` {number} + * `maxStreamsUni` {number} + * `maxStreamDataBidiLocal` {number} + * `maxStreamDataBidiRemote` {number} + * `maxStreamDataUni` {number} + * `minCidLen` {number} * `passphrase` {string} Shared passphrase used for a single private key and/or a PFX. * `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded @@ -511,6 +708,10 @@ added: REPLACEME occur in an array. `object.passphrase` is optional. Encrypted PFX will be decrypted with `object.passphrase` if provided, or `options.passphrase` if it is not. + * `preferredAddress` {Object} + * `address` {string} + * `port` {number} + * `type` {string} `'udp4'` or `'udp6'`. * `secureOptions` {number} Optionally affect the OpenSSL protocol behavior, which is not usually necessary. This should be used carefully if at all! Value is a numeric bitmask of the `SSL_OP_*` options from @@ -546,6 +747,9 @@ added: REPLACEME * `on` {boolean} +Sets or clears the `SO_BROADCAST` socket option. When set to `true`, UDP packets may be sent +to a local interface's broadcast address. + ### quicsocket.setMulticastLoopback([on]) +On most systems, where scope format uses the interface name: + +```js +const socket = quic.createSocket({ type: 'udp6', port: 1234 }); + +socket.on('ready', () => { + socket.setMulticastInterface('::%eth1'); +}); +``` + +On Windows, where scope format uses an interface number: + +```js +const socket = quic.createSocket({ type: 'udp6', port: 1234 }); + +socket.on('ready', () => { + socket.setMulticastInterface('::%2'); +}); +``` + +#### Example: IPv4 Outgoing Multicast Interface + +All systems use an IP of the host on the desired physical interface: + +```js +const socket = quic.createSocket({ type: 'udp4', port: 1234 }); + +socket.on('ready', () => { + socket.setMulticastInterface('10.0.0.2'); +}); +``` + +#### Call Results# + +A call on a socket that is not ready to send or no longer open may throw a Not running Error. + +If multicastInterface can not be parsed into an IP then an `EINVAL` System Error is thrown. + +On IPv4, if `multicastInterface` is a valid address but does not match any interface, or if +the address does not match the family then a System Error such as `EADDRNOTAVAIL` or +`EPROTONOSUP` is thrown. + +On IPv6, most errors with specifying or omitting scope will result in the socket continuing +to use (or returning to) the system's default interface selection. + +A socket's address family's ANY address (IPv4 `'0.0.0.0'` or IPv6 `'::'`) can be used to +return control of the sockets default outgoing interface to the system for future multicast packets. + ### quicsocket.setMulticastTTL(ttl) -### Class: QuicStream +## Class: QuicStream extends stream.Duplex @@ -606,6 +901,24 @@ added: REPLACEME added: REPLACEME --> +### quicstream.bidirectional + + +* Type: {boolean} + +True if the `QuicStream` is bidirectional. + +### quicstream.clientInitiated + + +* Type: {boolean} + +True if the `QuicStream` was initiated by a `QuicClientSession` instance. + ### quicstream.id + +* Type: {boolean} + +True if the `QuicStream` was initiated by a `QuicServerSession` instance. + ### quicstream.session * Type: {QuicSession} + +The `QuicServerSession` or `QuicClientSession`. + +### quicstream.unidirectional + + +* Type: {boolean} + +True if the `QuicStream` is unidirectional. + + + +[RFC 4007]: https://tools.ietf.org/html/rfc4007 +[Certificate Object]: https://nodejs.org/dist/latest-v12.x/docs/api/tls.html#tls_certificate_object diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 1f6ff6e894..0054fc3633 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1036,6 +1036,28 @@ E('ERR_QUICSOCKET_LISTENING', 'This QuicSocket is already listening', Error); E('ERR_QUICSOCKET_UNBOUND', 'Cannot call %s before a QuicSocket has been bound', Error); +E('ERR_QUIC_ERROR', function(code, family) { + const { + constants: { + QUIC_ERROR_APPLICATION, + QUIC_ERROR_CRYPTO, + QUIC_ERROR_SESSION, + } + } = internalBinding('quic'); + let familyType = 'unknown'; + switch (family) { + case QUIC_ERROR_APPLICATION: + familyType = 'application'; + break; + case QUIC_ERROR_CRYPTO: + familyType = 'crypto'; + break; + case QUIC_ERROR_SESSION: + familyType = 'session'; + break; + } + return `QUIC session closed with ${familyType} error code ${code}`; +}, Error); E('ERR_QUIC_TLS13_REQUIRED', 'QUIC requires TLS version 1.3', Error); E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s', Error); E('ERR_SCRIPT_EXECUTION_INTERRUPTED', diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index b8eb1ae45d..c84a792a50 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -9,11 +9,15 @@ const { assertCrypto(); +const { Error } = primordials; +const { Buffer } = require('buffer'); +const { isArrayBufferView } = require('internal/util/types'); const { getAllowUnauthorized, getSocketType, lookup4, lookup6, + validateCloseCode, validateTransportParams, validateQuicClientSessionOptions, validateQuicSocketOptions, @@ -58,6 +62,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_CALLBACK, + ERR_QUIC_ERROR, ERR_QUICSESSION_DESTROYED, ERR_QUICSOCKET_CLOSING, ERR_QUICSOCKET_DESTROYED, @@ -102,10 +107,14 @@ const { IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT, IDX_QUIC_SESSION_MAX_ACK_DELAY, IDX_QUIC_SESSION_STATE_CONNECTION_ID_COUNT, + IDX_QUIC_SESSION_STATE_CERT_ENABLED, + IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED, IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED, ERR_INVALID_REMOTE_TRANSPORT_PARAMS, ERR_INVALID_TLS_SESSION_TICKET, NGTCP2_PATH_VALIDATION_RESULT_FAILURE, + NGTCP2_NO_ERROR, + QUIC_ERROR_APPLICATION, } } = internalBinding('quic'); @@ -117,6 +126,8 @@ const emit = EventEmitter.prototype.emit; const kAddSession = Symbol('kAddSession'); const kAddStream = Symbol('kAddStream'); +const kCert = Symbol('kCert'); +const kClientHello = Symbol('kClientHello'); const kContinueBind = Symbol('kContinueBind'); const kContinueConnect = Symbol('kContinueConnect'); const kContinueListen = Symbol('kContinueListen'); @@ -132,6 +143,7 @@ const kReceiveStop = Symbol('kReceiveStop'); const kRemoveSession = Symbol('kRemove'); const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); +const kSetCloseCode = Symbol('kSetCloseCode'); const kSetSocket = Symbol('kSetSocket'); const kTrackWriteState = Symbol('kTrackWriteState'); const kWriteGeneric = Symbol('kWriteGeneric'); @@ -208,10 +220,79 @@ function onSessionReady(sessionHandle) { } // Called when a QuicSession is closed -function onSessionClose(code) { +function onSessionClose(code, family) { + this[owner_symbol][kSetCloseCode](code, family); this[owner_symbol].destroy(); } +// This callback is invoked at the start of the TLS handshake to provide +// some basic information about the ALPN, SNI, and Ciphers that are +// being requested. It is only called if the 'clientHello' event is +// listened for. +function onSessionClientHello(alpn, servername, ciphers, callback) { + callback = callback.bind(this); + this[owner_symbol][kClientHello]( + alpn, + servername, + ciphers, + (err, ...args) => { + if (err) { + this[owner_symbol].destroy(err); + return; + } + try { + callback(...args); + } catch (err) { + this[owner_symbol].destroy(err); + } + }); +} + +// This callback is only ever invoked for QuicServerSession instances, +// and is used to trigger OCSP request processing when needed. The +// user callback must invoke the callback function in order for the +// TLS handshake to continue. +function onSessionCert(servername, callback) { + callback = callback.bind(this); + this[owner_symbol][kCert](servername, (err, context, ocspResponse) => { + if (err) { + this[owner_symbol].destroy(err); + return; + } + if (context != null && !context.context) { + this[owner_symbol].destroy( + new ERR_INVALID_ARG_TYPE( + 'context', + 'SecureContext', + context)); + } + if (ocspResponse != null) { + if (typeof ocspResponse === 'string') + ocspResponse = Buffer.from(ocspResponse); + if (!isArrayBufferView(ocspResponse)) { + this[owner_symbol].destroy( + new ERR_INVALID_ARG_TYPE( + 'ocspResponse', + ['string', 'Buffer', 'TypedArray', 'DataView'], + ocspResponse)); + } + } + try { + callback(context ? context.context : undefined, ocspResponse); + } catch (err) { + this[owner_symbol].destroy(err); + } + }); +} + +// This callback is only ever invoked for QuicClientSession instances, +// and is used to deliver the OCSP response as provided by the server. +// If the requestOCSP configuration option is false, this will never +// be called. +function onSessionStatus(response) { + this[owner_symbol][kCert](response); +} + function onSessionHandshake( servername, alpn, @@ -257,7 +338,8 @@ function onSessionExtend(bidi, maxStreams) { } function onSessionKeylog(line) { - process.nextTick(emit.bind(this[owner_symbol], 'keylog', line)); + this[owner_symbol].emit('keylog', line); +// process.nextTick(emit.bind(this[owner_symbol], 'keylog', line)); } // Called when a new QuicStream is ready to use @@ -270,7 +352,10 @@ function onStreamReady(streamHandle, id) { assert(!session.closing); // TODO(@jasnell): Get default options from session - const stream = new QuicStream({ /* options */ }, session, id, streamHandle); + const uni = id & 0b10; + const stream = new QuicStream({ writable: !uni }, session, id, streamHandle); + if (uni) + stream.end(); session[kAddStream](id, stream); process.nextTick(emit.bind(session, 'stream', stream)); } @@ -297,11 +382,14 @@ setCallbacks({ onSocketClose, onSocketError, onSessionReady, + onSessionCert, + onSessionClientHello, onSessionClose, onSessionError, onSessionExtend, onSessionHandshake, onSessionKeylog, + onSessionStatus, onSessionTicket, onStreamReady, onStreamClose, @@ -368,6 +456,83 @@ function connectAfterBind(session, lookup, address, type) { connectAfterLookup.bind(session, type)); } +function createSecureContext(options, init_cb) { + const { + ca, + cert, + ciphers = DEFAULT_QUIC_CIPHERS, + clientCertEngine, + crl, + dhparam, + ecdhCurve, + groups = DEFAULT_GROUPS, + honorCipherOrder, + key, + passphrase, + pfx, + sessionIdContext, + secureProtocol + } = { ...options }; + + if (typeof ciphers !== 'string') + throw new ERR_INVALID_ARG_TYPE('option.ciphers', 'string', ciphers); + if (typeof groups !== 'string') + throw new ERR_INVALID_ARG_TYPE('option.groups', 'string', groups); + + const sc = _createSecureContext({ + secureProtocol, + ca, + cert, + ciphers: ciphers || DEFAULT_QUIC_CIPHERS, + clientCertEngine, + crl, + dhparam, + ecdhCurve, + honorCipherOrder, + key, + passphrase, + pfx, + sessionIdContext + }); + // Perform additional QUIC specific initialization on the SecureContext + init_cb(sc.context, groups || DEFAULT_GROUPS); + return sc; +} + +function onNewListener(event) { + if (this[kHandle] === undefined || this.listenerCount(event) !== 0) + return; + + switch (event) { + case 'keylog': + this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 1; + break; + case 'clientHello': + this[kHandle].state[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 1; + break; + case 'OCSPRequest': + this[kHandle].state[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 1; + break; + } +} + +function onRemoveListener(event) { + if (this[kHandle] === undefined || this.listenerCount(event) !== 0) + return; + + switch (event) { + case 'keylog': + this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 0; + break; + case 'clientHello': + this[kHandle].state[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 0; + break; + case 'OCSPRequest': + this[kHandle].state[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 0; + break; + } +} + // QuicSocket wraps a UDP socket plus the associated TLS context and QUIC // Protocol state. There may be *multiple* QUIC connections (QuicSession) // associated with a single QuicSocket. @@ -385,6 +550,7 @@ class QuicSocket extends EventEmitter { #sessions = new Set(); #state = kSocketUnbound; #type = undefined; + #alpn = undefined; constructor(options) { const { @@ -496,12 +662,19 @@ class QuicSocket extends EventEmitter { port, type = AF_INET, } = { ...preferredAddress }; + const { + rejectUnauthorized = !getAllowUnauthorized(), + requestCert = false, + } = transportParams; setTransportParams(transportParams); this[kHandle].listen( this.#serverSecureContext.context, address, type, - port); + port, + this.#alpn, + rejectUnauthorized, + requestCert); process.nextTick(emit.bind(this, 'listening')); } @@ -530,6 +703,10 @@ class QuicSocket extends EventEmitter { ...options }; + const { alpn } = options; + if (alpn !== undefined && typeof alpn !== 'string') + throw new ERR_INVALID_ARG_TYPE('options.alpn', 'string', alpn); + if (callback) { if (typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(); @@ -542,7 +719,7 @@ class QuicSocket extends EventEmitter { // since we do not need to access this anywhere else. this.#serverSecureContext = createSecureContext(options, initSecureContext); this.#serverListening = true; - + this.#alpn = alpn; const doListen = continueListen.bind( this, @@ -633,6 +810,10 @@ class QuicSocket extends EventEmitter { return; this.#state = kSocketClosing; + // Otherwise, gracefully close each QuicSession, with + // [kMaybeDestroy]() being called after each closes. + const maybeDestroy = this[kMaybeDestroy].bind(this); + // If there are no sessions, call [kMaybeDestroy]() // immediately to destroy the QuicSocket if (this.#sessions.size === 0) { @@ -641,9 +822,6 @@ class QuicSocket extends EventEmitter { return; } - // Otherwise, gracefully close each QuicSession, with - // [kMaybeDestroy]() being called after each closes. - const maybeDestroy = this[kMaybeDestroy].bind(this); for (const session of this.#sessions) session.close(maybeDestroy); } @@ -683,6 +861,10 @@ class QuicSocket extends EventEmitter { return this; } + get serverSecureContext() { + return this.#serverSecureContext; + } + get address() { const out = {}; if (this.#state !== kSocketDestroyed) { @@ -795,72 +977,12 @@ class QuicSocket extends EventEmitter { } } -function createSecureContext(options, init_cb) { - const { - ca, - cert, - ciphers = DEFAULT_QUIC_CIPHERS, - clientCertEngine, - crl, - dhparam, - ecdhCurve, - groups = DEFAULT_GROUPS, - honorCipherOrder, - key, - passphrase, - pfx, - sessionIdContext, - secureProtocol - } = { ...options }; - - if (typeof ciphers !== 'string') - throw new ERR_INVALID_ARG_TYPE('option.ciphers', 'string', ciphers); - if (typeof groups !== 'string') - throw new ERR_INVALID_ARG_TYPE('option.groups', 'string', groups); - - const sc = _createSecureContext({ - secureProtocol, - ca, - cert, - ciphers: ciphers || DEFAULT_QUIC_CIPHERS, - clientCertEngine, - crl, - dhparam, - ecdhCurve, - honorCipherOrder, - key, - passphrase, - pfx, - sessionIdContext - }); - // Perform additional QUIC specific initialization on the SecureContext - init_cb(sc.context, groups || DEFAULT_GROUPS); - return sc; -} - -function onNewKeylogListener(event) { - if (event !== 'keylog' || - this[kHandle] === undefined || - this.listenerCount('keylog') !== 0) { - return; - } - this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 1; -} - -function onRemoveKeylogListener(event) { - if (event !== 'keylog' || - this[kHandle] === undefined || - this.listenerCount('keylog') !== 0) { - return; - } - this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 0; -} - class QuicSession extends EventEmitter { #alpn = undefined; #cipher = undefined; #cipherVersion = undefined; - #closeCode = 0; + #closeCode = NGTCP2_NO_ERROR; + #closeFamily = QUIC_ERROR_APPLICATION; #closing = false; #destroyed = false; #handshakeComplete = false; @@ -873,13 +995,31 @@ class QuicSession extends EventEmitter { constructor(socket, servername) { super(); - this.on('newListener', onNewKeylogListener); - this.on('removeListener', onRemoveKeylogListener); + this.on('newListener', onNewListener); + this.on('removeListener', onRemoveListener); this.#socket = socket; socket[kAddSession](this); this.#servername = servername; } + [kSetCloseCode](code, family) { + this.#closeCode = code; + this.#closeFamily = family; + } + + [kInspect]() { + const obj = { + alpn: this.#alpn, + cipher: this.cipher, + closing: this.closing, + closeCode: this.closeCode, + destroyed: this.destroyed, + servername: this.servername, + streams: this.#streams.size, + }; + return `${this.constructor.name} ${util.format(obj)}`; + } + [kSetSocket](socket) { this.#socket = socket; } @@ -953,11 +1093,15 @@ class QuicSession extends EventEmitter { if (typeof code === 'function') { callback = code; - code = 0; + code = NGTCP2_NO_ERROR; } - if (code !== undefined && typeof code !== 'number') - throw new ERR_INVALID_ARG_TYPE('code', 'number', code); + const { + closeCode, + closeFamily + } = validateCloseCode(code); + this.#closeCode = closeCode; + this.#closeFamily = closeFamily; if (callback) { if (typeof callback !== 'function') @@ -1003,12 +1147,25 @@ class QuicSession extends EventEmitter { this.#destroyed = true; this.#closing = false; + if (typeof error === 'number' || + (error != null && + typeof error === 'object' && + !(error instanceof Error))) { + const { + closeCode, + closeFamily + } = validateCloseCode(error); + this.#closeCode = closeCode; + this.#closeFamily = closeFamily; + error = new ERR_QUIC_ERROR(closeCode, closeFamily); + } + // Destroy any remaining streams immediately for (const stream of this.#streams.values()) stream.destroy(error); - this.removeListener('newListener', onNewKeylogListener); - this.removeListener('removeListener', onRemoveKeylogListener); + this.removeListener('newListener', onNewListener); + this.removeListener('removeListener', onRemoveListener); const handle = this[kHandle]; if (handle !== undefined) { @@ -1017,7 +1174,7 @@ class QuicSession extends EventEmitter { // Calling destroy will cause a CONNECTION_CLOSE to be // sent to the peer and will destroy the QuicSession // handler immediately. - handle.destroy(); + handle.destroy(this.#closeCode, this.#closeFamily); } // Remove the QuicSession JavaScript object from the @@ -1066,6 +1223,13 @@ class QuicSession extends EventEmitter { return this.#closing; } + get closeCode() { + return { + code: this.#closeCode, + family: this.#closeFamily + }; + } + get socket() { return this.#socket; } @@ -1097,21 +1261,67 @@ class QuicSession extends EventEmitter { this, id, handle); + if (halfOpen) { + stream.push(null); + stream.read(); + } this.#streams.set(id, stream); return stream; } } class QuicServerSession extends QuicSession { + #contexts = []; constructor(socket, handle) { super(socket); this[kHandle] = handle; handle[owner_symbol] = this; } + [kClientHello](alpn, servername, ciphers, callback) { + this.emit( + 'clientHello', + alpn, + servername, + ciphers, + callback.bind(this[kHandle])); + } + [kReady]() { process.nextTick(emit.bind(this, 'ready')); } + + [kCert](servername, callback) { + const { serverSecureContext } = this.socket; + let { context } = serverSecureContext; + + for (var i = 0; i < this.#contexts.length; i++) { + const elem = this.#contexts[i]; + if (elem[0].test(servername)) + context = elem[1]; + break; + } + + this.emit( + 'OCSPRequest', + servername, + context, + callback.bind(this[kHandle])); + } + + addContext(servername, context = {}) { + if (typeof servername !== 'string') + throw new ERR_INVALID_ARG_TYPE('servername', 'string', servername); + + if (context == null || typeof context !== 'object') + throw new ERR_INVALID_ARG_TYPE('context', 'Object', context); + + const re = new RegExp('^' + + servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1') + .replace(/\*/g, '[^.]*') + + '$'); + this.#contexts.push([re, _createSecureContext(context)]); + } } function setSocketAfterBind(socket, callback) { @@ -1142,53 +1352,59 @@ function setSocketAfterBind(socket, callback) { } class QuicClientSession extends QuicSession { + #alpn = undefined; + #dcid = undefined; #handleReady = false; #ipv6Only = undefined; #minDHSize = undefined; #port = undefined; + #remoteTransportParams = undefined; + #requestOCSP = undefined; #secureContext = undefined; + #sessionTicket = undefined; #socketReady = false; #transportParams = undefined; - #sessionTicket = undefined; - #remoteTransportParams = undefined; - #dcid = undefined; #preferredAddressPolicy; constructor(socket, options) { const sc_options = { secureProtocol: 'TLSv1_3_client_method', - rejectUnauthorized: !getAllowUnauthorized(), ...options }; - const { - servername, - port, - ipv6Only, - minDHSize, - remoteTransportParams, - sessionTicket, + alpn, dcid, + ipv6Only, maxCidLen, minCidLen, + minDHSize, + port, preferredAddressPolicy, + remoteTransportParams, + requestOCSP, + servername, + sessionTicket, } = validateQuicClientSessionOptions(options); super(socket, servername); + this.#alpn = alpn; + this.#dcid = dcid; + this.#ipv6Only = ipv6Only; + this.#minDHSize = minDHSize; + this.#port = port || 0; + this.#preferredAddressPolicy = preferredAddressPolicy; + this.#remoteTransportParams = remoteTransportParams; + this.#requestOCSP = requestOCSP; + this.#secureContext = + createSecureContext( + sc_options, + initSecureContextClient); + this.#sessionTicket = sessionTicket; this.#transportParams = validateTransportParams( options, maxCidLen, minCidLen); - this.#ipv6Only = ipv6Only; - this.#minDHSize = minDHSize; - this.#port = port || 0; - this.#secureContext = createSecureContext(sc_options, - initSecureContextClient); - this.#sessionTicket = sessionTicket; - this.#remoteTransportParams = remoteTransportParams; - this.#dcid = dcid; - this.#preferredAddressPolicy = preferredAddressPolicy; } [kHandshakePost]() { @@ -1197,6 +1413,19 @@ class QuicClientSession extends QuicSession { this.destroy(new ERR_TLS_DH_PARAM_SIZE(size)); return false; } + + // TODO(@jasnell): QUIC *requires* that the client verify the + // identity of the server so we'll need to do that here. + // The current implementation of tls.checkServerIdentity is + // less than great and could be rewritten to speed it up + // significantly by running at the C++ layer. As it is + // currently, the method pulls the peer cert data, converts + // it to a javascript object, then processes the javascript + // object... which is more expensive than what is strictly + // necessary. + // + // See: _tls_wrap.js onConnectSecure function + return true; } @@ -1215,7 +1444,9 @@ class QuicClientSession extends QuicSession { this.#remoteTransportParams, this.#sessionTicket, this.#dcid, - this.#preferredAddressPolicy); + this.#preferredAddressPolicy, + this.#alpn, + this.#requestOCSP); // We no longer need these, unset them so // memory can be garbage collected. this.#remoteTransportParams = undefined; @@ -1251,6 +1482,10 @@ class QuicClientSession extends QuicSession { this[kMaybeReady](); } + [kCert](response) { + this.emit('OCSPResponse', response); + } + [kMaybeReady]() { if (this.#socketReady && this.#handleReady) process.nextTick(emit.bind(this, 'ready')); @@ -1297,6 +1532,16 @@ function afterShutdown() { stream[kMaybeDestroy](); } +function streamOnResume() { + if (!this.destroyed) + this[kHandle].readStart(); +} + +function streamOnPause() { + if (!this.destroyed /* && !this.pending */) + this[kHandle].readStop(); +} + class QuicStream extends Duplex { #didRead = false; #id = undefined; @@ -1307,7 +1552,8 @@ class QuicStream extends Duplex { ...options, allowHalfOpen: true, decodeStrings: true, - emitClose: true + emitClose: true, + autoDestroy: true, }); handle.onread = onStreamRead; handle[owner_symbol] = this; @@ -1316,10 +1562,32 @@ class QuicStream extends Duplex { this.#id = id; this.#session = session; this._readableState.readingMore = true; + this.on('pause', streamOnPause); + + // See src/node_quic_stream.h for an explanation + // of the initial states for unidirectional streams. + if (this.unidirectional) { + if (session instanceof QuicServerSession) { + if (this.serverInitiated) { + // Close the readable side + this.push(null); + this.read(); + } else { + // Close the writable side + this.end(); + } + } else if (this.serverInitiated) { + // Close the writable side + this.end(); + } else { + this.push(null); + this.read(); + } + } } get serverInitiated() { - return this.#id & 0b01; + return !!(this.#id & 0b01); } get clientInitiated() { @@ -1327,7 +1595,7 @@ class QuicStream extends Duplex { } get unidirectional() { - return this.#id & 0b10; + return !!(this.#id & 0b10); } get bidirectional() { @@ -1340,6 +1608,7 @@ class QuicStream extends Duplex { // remaining within the duplex writable side queue. this.end(); this.push(null); + this.read(); process.nextTick(emit.bind(this, 'reset', finalSize, appErrorCode)); // TODO(@jasnell): Should we destroy here? It's not yet clear // what else should be done @@ -1350,8 +1619,14 @@ class QuicStream extends Duplex { } [kInspect]() { + const direction = this.bidirectional ? 'bidirectional' : 'unidirectional'; + const initiated = this.serverInitiated ? 'server' : 'client'; const obj = { - id: this.#id + id: this.#id, + direction, + initiated, + writableState: this._writableState, + readableState: this._readableState, }; return `QuicStream ${util.format(obj)}`; } @@ -1412,7 +1687,13 @@ class QuicStream extends Duplex { this._readableState.readingMore = false; this.#didRead = true; } - this[kHandle].readStart(); + + streamOnResume.call(this); + // if (!this.pending) { + // streamOnResume.call(this); + // } else { + // this.once('ready', streamOnResume); + // } } get bufferSize() { @@ -1433,8 +1714,8 @@ class QuicStream extends Duplex { const handle = this[kHandle]; if (handle !== undefined) { this[kHandle] = undefined; - handle[owner_symbol] = undefined; handle.destroy(); + handle[owner_symbol] = undefined; } callback(error); } diff --git a/lib/internal/quic/util.js b/lib/internal/quic/util.js index 358bab7cb7..32b1da072c 100644 --- a/lib/internal/quic/util.js +++ b/lib/internal/quic/util.js @@ -32,11 +32,13 @@ const { IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT, MAX_RETRYTOKEN_EXPIRATION, MIN_RETRYTOKEN_EXPIRATION, + NGTCP2_NO_ERROR, NGTCP2_DEFAULT_MAX_ACK_DELAY, NGTCP2_MAX_CIDLEN, NGTCP2_MIN_CIDLEN, QUIC_PREFERRED_ADDRESS_IGNORE, QUIC_PREFERRED_ADDRESS_ACCEPT, + QUIC_ERROR_APPLICATION, } } = internalBinding('quic'); @@ -81,6 +83,24 @@ function lookup6(address, callback) { lookup(address || '::1', 6, callback); } +function validateCloseCode(code) { + let closeCode; + let closeFamily; + if (code != null && typeof code === 'object') { + closeCode = code.code || NGTCP2_NO_ERROR; + closeFamily = code.family || QUIC_ERROR_APPLICATION; + } else if (typeof code === 'number') { + closeCode = code; + closeFamily = QUIC_ERROR_APPLICATION; + } else { + throw new ERR_INVALID_ARG_TYPE('code', ['number', 'Object'], code); + } + return { + closeCode, + closeFamily + }; +} + function validateBindOptions(port, address) { if (!isLegalPort(port)) { throw new ERR_INVALID_ARG_VALUE( @@ -124,6 +144,8 @@ function validateTransportParams(params, maxCidLen, minCidLen) { IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT, maxAckDelay = NGTCP2_DEFAULT_MAX_ACK_DELAY, preferredAddress, + rejectUnauthorized, + requestCert, } = { ...params }; validateNumberInRange( maxStreamDataBidiLocal, @@ -174,22 +196,26 @@ function validateTransportParams(params, maxCidLen, minCidLen) { preferredAddress, maxCidLen, minCidLen, + rejectUnauthorized, + requestCert, }; } function validateQuicClientSessionOptions(options) { const { address, - servername = address, - port = 0, - ipv6Only = false, - minDHSize = 1024, - remoteTransportParams, - sessionTicket, + alpn, dcid: dcid_value, + ipv6Only = false, maxCidLen = NGTCP2_MAX_CIDLEN, minCidLen = NGTCP2_MIN_CIDLEN, + minDHSize = 1024, + port = 0, preferredAddressPolicy = 'ignore', + remoteTransportParams, + requestOCSP = false, + servername = address, + sessionTicket, } = { ...options }; if (typeof minDHSize !== 'number') @@ -224,6 +250,9 @@ function validateQuicClientSessionOptions(options) { sessionTicket); } + if (alpn !== undefined && typeof alpn !== 'string') + throw new ERR_INVALID_ARG_TYPE('options.alpn', 'string', alpn); + validateNumberInBoundedRange( maxCidLen, 'options.maxCidLen', @@ -274,21 +303,30 @@ function validateQuicClientSessionOptions(options) { preferredAddressPolicy); } + if (typeof requestOCSP !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE( + 'options.requestOCSP', + 'boolean', + requestOCSP); + } + return { address, - servername, - port, - ipv6Only, - minDHSize, - remoteTransportParams, - sessionTicket, + alpn, dcid, + ipv6Only, maxCidLen, minCidLen, + minDHSize, + port, preferredAddressPolicy: preferredAddressPolicy === 'accept' ? QUIC_PREFERRED_ADDRESS_ACCEPT : QUIC_PREFERRED_ADDRESS_IGNORE, + remoteTransportParams, + requestOCSP, + servername, + sessionTicket, }; } @@ -352,6 +390,7 @@ module.exports = { lookup4, lookup6, validateBindOptions, + validateCloseCode, validateNumberInRange, validateTransportParams, validateQuicClientSessionOptions, diff --git a/lib/internal/stream_base_commons.js b/lib/internal/stream_base_commons.js index 88896083f1..74a21326db 100644 --- a/lib/internal/stream_base_commons.js +++ b/lib/internal/stream_base_commons.js @@ -92,7 +92,7 @@ function onWriteComplete(status) { this.callback(null); } -function createWriteWrap(handle) { +function createWriteWrap(handle, callback) { const req = new WriteWrap(); req.handle = handle; @@ -100,12 +100,13 @@ function createWriteWrap(handle) { req.async = false; req.bytes = 0; req.buffer = null; + req.callback = callback; return req; } function writevGeneric(self, data, cb) { - const req = createWriteWrap(self[kHandle]); + const req = createWriteWrap(self[kHandle], cb); const allBuffers = data.allBuffers; var chunks; var i; @@ -126,29 +127,28 @@ function writevGeneric(self, data, cb) { // Retain chunks if (err === 0) req._chunks = chunks; - afterWriteDispatched(self, req, err, cb); + afterWriteDispatched(self, req, err); return req; } function writeGeneric(self, data, encoding, cb) { - const req = createWriteWrap(self[kHandle]); + const req = createWriteWrap(self[kHandle], cb); const err = handleWriteReq(req, data, encoding); - - afterWriteDispatched(self, req, err, cb); + afterWriteDispatched(self, req, err); return req; } -function afterWriteDispatched(self, req, err, cb) { +function afterWriteDispatched(self, req, err) { req.bytes = streamBaseState[kBytesWritten]; req.async = !!streamBaseState[kLastWriteWasAsync]; if (err !== 0) - return self.destroy(errnoException(err, 'write', req.error), cb); + return self.destroy( + errnoException(err, 'write', req.error), + req.callback()); - if (!req.async) { - cb(); - } else { - req.callback = cb; + if (!req.async && typeof req.callback === 'function') { + req.callback(); } } diff --git a/src/aliased_buffer.h b/src/aliased_buffer.h index 868d495be9..231a56e89d 100644 --- a/src/aliased_buffer.h +++ b/src/aliased_buffer.h @@ -32,6 +32,32 @@ template ::value>> class AliasedBufferBase { public: + /** + * Create an AliasedBufferBase over an existing buffer + */ + AliasedBufferBase( + v8::Isolate* isolate, + const size_t count, + NativeT* buffer) : + isolate_(isolate), + count_(count), + byte_offset_(0), + buffer_(buffer) { + CHECK_GT(count, 0); + const v8::HandleScope handle_scope(isolate_); + const size_t size_in_bytes = + MultiplyWithOverflowCheck(sizeof(NativeT), count); + + v8::Local ab = + v8::ArrayBuffer::New( + isolate_, + buffer, + count); + + v8::Local js_array = V8T::New(ab, byte_offset_, count); + js_array_ = v8::Global(isolate, js_array); + } + AliasedBufferBase(v8::Isolate* isolate, const size_t count) : isolate_(isolate), count_(count), byte_offset_(0) { CHECK_GT(count, 0); diff --git a/src/debug_utils.h b/src/debug_utils.h index ef5a4c0c47..bd942d8791 100644 --- a/src/debug_utils.h +++ b/src/debug_utils.h @@ -48,6 +48,22 @@ inline void Debug(Environment* env, Debug(env, cat, format.c_str(), std::forward(args)...); } +inline void Debug(Environment* env, + DebugCategory cat, + const char* format, + va_list args) { + if (!UNLIKELY(env->debug_enabled(cat))) + return; + vfprintf(stderr, format, args); +} + +inline void Debug(Environment* env, + DebugCategory cat, + const std::string& format, + va_list args) { + Debug(env, cat, format.c_str(), args); +} + // Used internally by the 'real' Debug(AsyncWrap*, ...) functions below, so that // the FORCE_INLINE flag on them doesn't apply to the contents of this function // as well. diff --git a/src/env.h b/src/env.h index a847a863b1..2abe2771ae 100644 --- a/src/env.h +++ b/src/env.h @@ -313,6 +313,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(stack_string, "stack") \ V(start_time_string, "startTime") \ V(state_string, "state") \ + V(stats_string, "stats") \ V(status_string, "status") \ V(stdio_string, "stdio") \ V(subject_string, "subject") \ @@ -415,11 +416,14 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(quic_on_socket_close_function, v8::Function) \ V(quic_on_socket_error_function, v8::Function) \ V(quic_on_session_ready_function, v8::Function) \ + V(quic_on_session_cert_function, v8::Function) \ + V(quic_on_session_client_hello_function, v8::Function) \ V(quic_on_session_close_function, v8::Function) \ V(quic_on_session_error_function, v8::Function) \ V(quic_on_session_extend_function, v8::Function) \ V(quic_on_session_handshake_function, v8::Function) \ V(quic_on_session_keylog_function, v8::Function) \ + V(quic_on_session_status_function, v8::Function) \ V(quic_on_session_ticket_function, v8::Function) \ V(quic_on_session_path_validation_function, v8::Function) \ V(quic_on_stream_ready_function, v8::Function) \ @@ -519,7 +523,8 @@ struct CompileFnEntry { #define DEBUG_CATEGORY_NAMES(V) \ NODE_ASYNC_PROVIDER_TYPES(V) \ V(INSPECTOR_SERVER) \ - V(INSPECTOR_PROFILER) + V(INSPECTOR_PROFILER) \ + V(NGTCP2_DEBUG) enum class DebugCategory { #define V(name) name, diff --git a/src/node_crypto.cc b/src/node_crypto.cc index b86d228bb4..47006f04aa 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -116,12 +116,6 @@ struct StackOfXASN1Deleter { }; using StackOfASN1 = std::unique_ptr; -// OPENSSL_free is a macro, so we need a wrapper function. -struct OpenSSLBufferDeleter { - void operator()(char* pointer) const { OPENSSL_free(pointer); } -}; -using OpenSSLBuffer = std::unique_ptr; - static const char* const root_certs[] = { #include "node_root_certs.h" // NOLINT(build/include_order) }; @@ -460,15 +454,6 @@ bool EntropySource(unsigned char* buffer, size_t length) { return RAND_bytes(buffer, length) != -1; } - -template -static T* MallocOpenSSL(size_t count) { - void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); - CHECK_IMPLIES(mem == nullptr, count == 0); - return static_cast(mem); -} - - void SecureContext::Initialize(Environment* env, Local target) { Local t = env->NewFunctionTemplate(New); t->InstanceTemplate()->SetInternalFieldCount(1); @@ -959,7 +944,6 @@ static X509_STORE* NewRootCertStore() { X509_STORE_add_cert(store, cert); } } - return store; } diff --git a/src/node_crypto.h b/src/node_crypto.h index 4416faaf95..a8f41e2ca5 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -73,6 +73,12 @@ using ECPointPointer = DeleteFnPtr; using ECKeyPointer = DeleteFnPtr; using DHPointer = DeleteFnPtr; +// OPENSSL_free is a macro, so we need a wrapper function. +struct OpenSSLBufferDeleter { + void operator()(char* pointer) const { OPENSSL_free(pointer); } +}; +using OpenSSLBuffer = std::unique_ptr; + extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx); extern void UseExtraCaCerts(const std::string& file); @@ -855,6 +861,14 @@ v8::Local GetLastIssuedCert( SSL* ssl, v8::Local issuer_chain, Environment* const env); + +template +inline T* MallocOpenSSL(size_t count) { + void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); + CHECK_IMPLIES(mem == nullptr, count == 0); + return static_cast(mem); +} + } // namespace crypto } // namespace node diff --git a/src/node_quic.cc b/src/node_quic.cc index ebba2cb0ef..9bdcc84ecc 100755 --- a/src/node_quic.cc +++ b/src/node_quic.cc @@ -47,12 +47,15 @@ void QuicSetCallbacks(const FunctionCallbackInfo& args) { SETFUNCTION("onSocketClose", socket_close); SETFUNCTION("onSocketError", socket_error); SETFUNCTION("onSessionReady", session_ready); + SETFUNCTION("onSessionCert", session_cert); + SETFUNCTION("onSessionClientHello", session_client_hello); SETFUNCTION("onSessionClose", session_close); SETFUNCTION("onSessionError", session_error); SETFUNCTION("onSessionExtend", session_extend); SETFUNCTION("onSessionHandshake", session_handshake); SETFUNCTION("onSessionKeylog", session_keylog); SETFUNCTION("onSessionPathValidation", session_path_validation); + SETFUNCTION("onSessionStatus", session_status); SETFUNCTION("onSessionTicket", session_ticket); SETFUNCTION("onStreamReady", stream_ready); SETFUNCTION("onStreamClose", stream_close); @@ -71,6 +74,23 @@ void QuicALPNVersion(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(OneByteString(env->isolate(), NGTCP2_ALPN_H3)); } +inline int Client_Hello_CB( + SSL* ssl, + int* tls_alert, + void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + int ret = session->OnClientHello(); + switch (ret) { + case 0: + return 1; + case -1: + return -1; + default: + *tls_alert = ret; + return 0; + } +} + int ALPN_Select_Proto_CB(SSL* ssl, const unsigned char** out, unsigned char* outlen, @@ -84,8 +104,8 @@ int ALPN_Select_Proto_CB(SSL* ssl, switch (version) { case NGTCP2_PROTO_VER: - alpn = reinterpret_cast(NGTCP2_ALPN_H3); - alpnlen = strsize(NGTCP2_ALPN_H3); + alpn = reinterpret_cast(session->GetALPN().c_str()); + alpnlen = session->GetALPN().length(); break; default: // Unexpected QUIC protocol version @@ -140,6 +160,11 @@ int Client_Transport_Params_Add_CB( return 1; } +int TLS_Status_Callback(SSL* ssl, void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + return session->OnTLSStatus(); +} + int Server_Transport_Params_Add_CB( SSL* ssl, unsigned int ext_type, @@ -270,6 +295,9 @@ void QuicInitSecureContext(const FunctionCallbackInfo& args) { SSL_CTX_set_default_verify_paths(**sc); SSL_CTX_set_max_early_data(**sc, std::numeric_limits::max()); SSL_CTX_set_alpn_select_cb(**sc, ALPN_Select_Proto_CB, nullptr); + SSL_CTX_set_client_hello_cb(**sc, Client_Hello_CB, nullptr); + SSL_CTX_set_tlsext_status_cb(**sc, TLS_Status_Callback); + SSL_CTX_set_tlsext_status_arg(**sc, nullptr); CHECK_EQ( SSL_CTX_add_custom_ext( **sc, @@ -300,6 +328,8 @@ void QuicInitSecureContextClient(const FunctionCallbackInfo& args) { SSL_CTX_set_mode(**sc, SSL_MODE_QUIC_HACK); SSL_CTX_clear_options(**sc, SSL_OP_ENABLE_MIDDLEBOX_COMPAT); SSL_CTX_set_default_verify_paths(**sc); + SSL_CTX_set_tlsext_status_cb(**sc, TLS_Status_Callback); + SSL_CTX_set_tlsext_status_arg(**sc, nullptr); CHECK_EQ(SSL_CTX_add_custom_ext( **sc, @@ -381,11 +411,17 @@ void Initialize(Local target, NODE_DEFINE_CONSTANT(constants, ERR_INVALID_REMOTE_TRANSPORT_PARAMS); NODE_DEFINE_CONSTANT(constants, ERR_INVALID_TLS_SESSION_TICKET); NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CONNECTION_ID_COUNT); + NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CERT_ENABLED); + NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED); NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED); NODE_DEFINE_CONSTANT(constants, MAX_RETRYTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(constants, MIN_RETRYTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(constants, NGTCP2_MAX_CIDLEN); NODE_DEFINE_CONSTANT(constants, NGTCP2_MIN_CIDLEN); + NODE_DEFINE_CONSTANT(constants, NGTCP2_NO_ERROR); + NODE_DEFINE_CONSTANT(constants, QUIC_ERROR_APPLICATION); + NODE_DEFINE_CONSTANT(constants, QUIC_ERROR_CRYPTO); + NODE_DEFINE_CONSTANT(constants, QUIC_ERROR_SESSION); NODE_DEFINE_CONSTANT(constants, QUIC_PREFERRED_ADDRESS_ACCEPT); NODE_DEFINE_CONSTANT(constants, QUIC_PREFERRED_ADDRESS_IGNORE); NODE_DEFINE_CONSTANT(constants, NGTCP2_DEFAULT_MAX_ACK_DELAY); diff --git a/src/node_quic_buffer.h b/src/node_quic_buffer.h index 3993cec103..5b86e6271f 100644 --- a/src/node_quic_buffer.h +++ b/src/node_quic_buffer.h @@ -187,6 +187,19 @@ class QuicBuffer : public MemoryRetainer { CHECK_EQ(length_, 0); } + inline uint64_t Copy( + uv_buf_t* bufs, + size_t nbufs) { + uint64_t total = 0; + for (size_t n = 0; n < nbufs; n++) { + MallocedBuffer data(bufs[n].len); + memcpy(data.data, bufs[n].base, bufs[n].len); + total += bufs[n].len; + Push(std::move(data)); + } + return total; + } + // Push one or more uv_buf_t instances into the buffer. // the done_cb callback will be invoked when the last // uv_buf_t in the bufs array is consumed and popped out diff --git a/src/node_quic_crypto.h b/src/node_quic_crypto.h index 4cfe6e46dd..0e91b4bb4f 100644 --- a/src/node_quic_crypto.h +++ b/src/node_quic_crypto.h @@ -896,6 +896,58 @@ inline void MessageCB( } } +inline std::string ToHex(const uint8_t* s, size_t len) { + static constexpr char LOWER_XDIGITS[] = "0123456789abcdef"; + std::string res; + res.resize(len * 2); + for (size_t i = 0; i < len; ++i) { + auto c = s[i]; + res[i * 2] = LOWER_XDIGITS[c >> 4]; + res[i * 2 + 1] = LOWER_XDIGITS[c & 0x0f]; + } + return res; +} + +inline void LogSecret( + SSL* ssl, + int name, + const unsigned char* secret, + size_t secretlen) { + if (auto keylog_cb = SSL_CTX_get_keylog_callback(SSL_get_SSL_CTX(ssl))) { + unsigned char crandom[32]; + if (SSL_get_client_random(ssl, crandom, 32) != 32) + return; + std::string line; + switch (name) { + case SSL_KEY_CLIENT_EARLY_TRAFFIC: + line = "QUIC_CLIENT_EARLY_TRAFFIC_SECRET"; + break; + case SSL_KEY_CLIENT_HANDSHAKE_TRAFFIC: + line = "QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET"; + break; + case SSL_KEY_CLIENT_APPLICATION_TRAFFIC: + line = "QUIC_CLIENT_TRAFFIC_SECRET_0"; + break; + case SSL_KEY_SERVER_HANDSHAKE_TRAFFIC: + line = "QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET"; + break; + case SSL_KEY_SERVER_APPLICATION_TRAFFIC: + line = "QUIC_SERVER_TRAFFIC_SECRET_0"; + break; + default: + return; + } + line += " " + ToHex(crandom, 32); + line += " " + ToHex(secret, secretlen); + keylog_cb(ssl, line.c_str()); + } +} + +inline int CertCB(SSL* ssl, void* arg) { + QuicSession* session = static_cast(arg); + return session->OnCert(); +} + // KeyCB provides a hook into the keying process of the TLS handshake, // triggering registration of the keys associated with the TLS session. inline int KeyCB( @@ -906,9 +958,211 @@ inline int KeyCB( void* arg) { QuicSession* session = static_cast(arg); + // Output the secret to the keylog + LogSecret(ssl, name, secret, secretlen); + return session->OnKey(name, secret, secretlen) != 0 ? 0 : 1; } +inline int ClearTLS(SSL* ssl, bool continue_on_error = false) { + std::array buf; + size_t nread; + for (;;) { + int err = SSL_read_ex(ssl, buf.data(), buf.size(), &nread); + if (err == 1) { + if (continue_on_error) + continue; + return NGTCP2_ERR_PROTO; + } + int code = SSL_get_error(ssl, 0); + switch (code) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + case SSL_ERROR_WANT_X509_LOOKUP: + return 0; + case SSL_ERROR_SSL: + case SSL_ERROR_ZERO_RETURN: + return NGTCP2_ERR_CRYPTO; + default: + return NGTCP2_ERR_CRYPTO; + } + } + return 0; +} + +inline int DoTLSHandshake(SSL* ssl) { + int err = SSL_do_handshake(ssl); + if (err <= 0) { + err = SSL_get_error(ssl, err); + switch (err) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + // For the next two, the handshake has been suspended but + // the data was otherwise successfully read, so return 0 + // here but the handshake won't continue until we trigger + // things on our side. + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + case SSL_ERROR_WANT_X509_LOOKUP: + return 0; + case SSL_ERROR_SSL: + return NGTCP2_ERR_CRYPTO; + default: + return NGTCP2_ERR_CRYPTO; + } + } + return err; +} + +inline int DoTLSReadEarlyData(SSL* ssl) { + std::array buf; + size_t nread; + int err = SSL_read_early_data(ssl, buf.data(), buf.size(), &nread); + switch (err) { + case SSL_READ_EARLY_DATA_ERROR: { + int code = SSL_get_error(ssl, err); + switch (code) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + // For the next two, the handshake has been suspended but + // the data was otherwise successfully read, so return 0 + // here but the handshake won't continue until we trigger + // things on our side. + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + case SSL_ERROR_WANT_X509_LOOKUP: + return 0; + case SSL_ERROR_SSL: + return NGTCP2_ERR_CRYPTO; + default: + return NGTCP2_ERR_CRYPTO; + } + break; + } + case SSL_READ_EARLY_DATA_SUCCESS: + if (nread > 0) + return NGTCP2_ERR_PROTO; + break; + case SSL_READ_EARLY_DATA_FINISH: + break; + } + return 0; +} + +inline crypto::OpenSSLBuffer GetClientHelloRandom(SSL* ssl) { + const unsigned char* buf; + SSL_client_hello_get0_random(ssl, &buf); + return crypto::OpenSSLBuffer( + const_cast(reinterpret_cast(buf))); +} + +inline crypto::OpenSSLBuffer GetClientHelloSessionID(SSL* ssl) { + const unsigned char* buf; + SSL_client_hello_get0_session_id(ssl, &buf); + return crypto::OpenSSLBuffer( + const_cast(reinterpret_cast(buf))); +} + +inline v8::Local GetClientHelloCiphers( + Environment* env, + SSL* ssl) { + v8::Local ciphers_array; + const unsigned char* buf; + size_t len = SSL_client_hello_get0_ciphers(ssl, &buf); + if (len == 0) + return ciphers_array; + + ciphers_array = v8::Array::New(env->isolate(), len / 2); + size_t pos = 0; + for (size_t n = 0; n < len; n += 2) { + auto cipher = SSL_CIPHER_find(ssl, buf); + buf += 2; + const char* cipher_name = SSL_CIPHER_get_name(cipher); + const char* cipher_version = SSL_CIPHER_get_version(cipher); + v8::Local obj = v8::Object::New(env->isolate()); + USE(obj->Set( + env->context(), + env->name_string(), + OneByteString(env->isolate(), cipher_name))); + USE(obj->Set( + env->context(), + env->version_string(), + OneByteString(env->isolate(), cipher_version))); + USE(ciphers_array->Set(env->context(), pos++, obj)); + } + + return ciphers_array; +} + +inline crypto::OpenSSLBuffer GetClientHelloCompressionMethods(SSL* ssl) { + const unsigned char* buf; + SSL_client_hello_get0_compression_methods(ssl, &buf); + return crypto::OpenSSLBuffer( + const_cast(reinterpret_cast(buf))); +} + +inline const char* GetClientHelloServerName(SSL* ssl) { + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &buf, &rem) || + rem <= 2) + return nullptr; + + len = (*(buf++) << 8); + len += *(buf++); + if (len + 2 != rem) + return nullptr; + rem = len; + + if (rem == 0 || *buf++ != TLSEXT_NAMETYPE_host_name) + return nullptr; + rem--; + if (rem <= 2) + return nullptr; + len = (*(buf++) << 8); + len += *(buf++); + if (len + 2 > rem) + return nullptr; + rem = len; + return reinterpret_cast(buf); +} + +inline const char* GetClientHelloALPN(SSL* ssl) { + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext( + ssl, + TLSEXT_TYPE_application_layer_protocol_negotiation, + &buf, &rem) || rem < 2) { + return nullptr; + } + + len = (buf[0] << 8) | buf[1]; + if (len + 2 != rem) + return nullptr; + buf += 3; + return reinterpret_cast(buf); +} + +inline int UseSNIContext(SSL* ssl, crypto::SecureContext* context) { + SSL_CTX* ctx = context->ctx_.get(); + X509* x509 = SSL_CTX_get0_certificate(ctx); + EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx); + STACK_OF(X509)* chain; + + int err = SSL_CTX_get0_chain_certs(ctx, &chain); + if (err) + err = SSL_use_certificate(ssl, x509); + if (err) + err = SSL_use_PrivateKey(ssl, pkey); + if (err && chain != nullptr) + err = SSL_set1_chain(ssl, chain); + return err; +} + } // namespace quic } // namespace node diff --git a/src/node_quic_session.cc b/src/node_quic_session.cc index 1caadc84f1..641a5bfa6e 100644 --- a/src/node_quic_session.cc +++ b/src/node_quic_session.cc @@ -27,9 +27,11 @@ namespace node { using crypto::EntropySource; using crypto::SecureContext; +using v8::Array; using v8::ArrayBufferView; using v8::Context; using v8::Float64Array; +using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; @@ -40,6 +42,7 @@ using v8::Object; using v8::ObjectTemplate; using v8::PropertyAttribute; using v8::String; +using v8::Undefined; using v8::Value; namespace quic { @@ -66,6 +69,11 @@ void QuicSessionConfig::Set( env->quic_state()->quicsessionconfig_buffer; uint64_t flags = buffer[IDX_QUIC_SESSION_CONFIG_COUNT]; +// The following might be non-obvious. The QUICSESSION_CONFIG macro defines +// the set of numeric configuration values for the QuicSessionConfig. They +// are defined this way to avoid code duplicate in a couple of places. The +// following macro expands out to set each member value in the QuicSessionConfig +// to the corresponding value in the AliasedBuffer #define V(idx, name, def) \ if (flags & (1 << IDX_QUIC_SESSION_##idx)) \ name##_ = static_cast(buffer[IDX_QUIC_SESSION_##idx]); @@ -144,15 +152,15 @@ void QuicSessionConfig::ToSettings(ngtcp2_settings* settings, } void QuicSession::CheckAllocatedSize(size_t previous_size) { - // CHECK_GE(current_ngtcp2_memory_, previous_size); + CHECK_GE(current_ngtcp2_memory_, previous_size); } void QuicSession::IncrementAllocatedSize(size_t size) { - // current_ngtcp2_memory_ += size; + current_ngtcp2_memory_ += size; } void QuicSession::DecrementAllocatedSize(size_t size) { - // current_ngtcp2_memory_ -= size; + current_ngtcp2_memory_ -= size; } // Static ngtcp2 callbacks are registered when ngtcp2 when a new ngtcp2_conn is @@ -239,6 +247,17 @@ int QuicSession::OnExtendMaxStreamsUni( return 0; } +int QuicSession::OnExtendMaxStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + session->ExtendMaxStreamData(stream_id, max_data); + return 0; +} + // Called by ngtcp2 for both client and server connections // when ngtcp2 has determined that the TLS handshake has // been completed. @@ -415,7 +434,7 @@ int QuicSession::OnReceiveStreamData( void* stream_user_data) { QuicSession* session = static_cast(user_data); RETURN_IF_FAIL( - session->ReceiveStreamData(stream_id, fin, offset, data, datalen), 0, + session->ReceiveStreamData(stream_id, fin, data, datalen), 0, NGTCP2_ERR_CALLBACK_FAILURE); return 0; } @@ -455,25 +474,22 @@ int QuicSession::OnAckedStreamDataOffset( void* user_data, void* stream_user_data) { QuicSession* session = static_cast(user_data); - RETURN_IF_FAIL( - session->AckedStreamDataOffset(stream_id, offset, datalen), 0, - NGTCP2_ERR_CALLBACK_FAILURE); + session->AckedStreamDataOffset(stream_id, offset, datalen); return 0; } // Called by ngtcp2 for a client connection when the server // has indicated a preferred address in the transport // params. +// For now, there are two modes: we can accept the preferred address +// or we can reject it. Later, we may want to implement a callback +// to ask the user if they want to accept the preferred address or +// not. int QuicSession::OnSelectPreferredAddress( ngtcp2_conn* conn, ngtcp2_addr* dest, const ngtcp2_preferred_addr* paddr, void* user_data) { - - // For now, there are two modes: we can accept the preferred address - // or we can reject it. Later, we may want to implement a callback - // to ask the user if they want to accept the preferred address or - // not. QuicSession* session = static_cast(user_data); RETURN_IF_FAIL( session->SelectPreferredAddress(dest, paddr), 0, @@ -643,10 +659,8 @@ int QuicSession::VerifyRetryToken( SocketAddress::GetAddress(addr, &host); } - if (hd->tokenlen < TOKEN_RAND_DATALEN) { - // token is too short + if (hd->tokenlen < TOKEN_RAND_DATALEN) return -1; - } uint8_t* rand_data = hd->token + hd->tokenlen - TOKEN_RAND_DATALEN; uint8_t* ciphertext = hd->token; @@ -674,26 +688,19 @@ int QuicSession::VerifyRetryToken( params.iv.data(), params.ivlen, reinterpret_cast(addr), addrlen); - if (n < 0) { - // Could not decrypt token + if (n < 0) return -1; - } - if (static_cast(n) < addrlen + sizeof(uint64_t)) { - // Bad token construction + if (static_cast(n) < addrlen + sizeof(uint64_t)) return -1; - } ssize_t cil = static_cast(n) - addrlen - sizeof(uint64_t); - if (cil != 0 && (cil < NGTCP2_MIN_CIDLEN || cil > NGTCP2_MAX_CIDLEN)) { - // Bad token construction + if (cil != 0 && (cil < NGTCP2_MIN_CIDLEN || cil > NGTCP2_MAX_CIDLEN)) return -1; - } - if (memcmp(plaintext.data(), addr, addrlen) != 0) { - // Client address does not match + if (memcmp(plaintext.data(), addr, addrlen) != 0) return -1; - } + uint64_t t; memcpy(&t, plaintext.data() + addrlen, sizeof(uint64_t)); @@ -703,10 +710,8 @@ int QuicSession::VerifyRetryToken( // 10-second window by default, but configurable for each // QuicSocket instance with a MIN_RETRYTOKEN_EXPIRATION second // minimum and a MAX_RETRYTOKEN_EXPIRATION second maximum. - if (t + verification_expiration * NGTCP2_SECONDS < now) { - // Token has expired + if (t + verification_expiration * NGTCP2_SECONDS < now) return -1; - } return 0; } @@ -717,19 +722,19 @@ QuicSession::QuicSession( QuicSocket* socket, Local wrap, SecureContext* ctx, - AsyncWrap::ProviderType type) : + AsyncWrap::ProviderType type, + const std::string& alpn) : AsyncWrap(socket->env(), wrap, type), rx_crypto_level_(NGTCP2_CRYPTO_LEVEL_INITIAL), tx_crypto_level_(NGTCP2_CRYPTO_LEVEL_INITIAL), + last_error_(QUIC_ERROR_SESSION, NGTCP2_NO_ERROR), closing_(false), destroyed_(false), initial_(true), connection_(nullptr), - tls_alert_(0), max_pktlen_(0), idle_timer_(nullptr), socket_(socket), - nkey_update_(0), hs_crypto_ctx_{}, crypto_ctx_{}, txbuf_(new QuicBuffer()), @@ -740,7 +745,15 @@ QuicSession::QuicSession( current_ngtcp2_memory_(0), max_cid_len_(NGTCP2_MAX_CIDLEN), min_cid_len_(NGTCP2_MIN_CIDLEN), - allocator_(this) { + alpn_(alpn), + allocator_(this), + cert_cb_running_(false), + client_hello_cb_running_(false), + is_tls_callback_(false), + stats_buffer_( + socket->env()->isolate(), + sizeof(session_stats_) / sizeof(uint64_t), + reinterpret_cast(&session_stats_)) { ssl_.reset(SSL_new(ctx->ctx_.get())); SSL_CTX_set_keylog_callback(ctx->ctx_.get(), OnKeylog); CHECK(ssl_); @@ -751,6 +764,14 @@ QuicSession::QuicSession( state_.GetJSArray(), PropertyAttribute::ReadOnly)); + session_stats_.created_at = uv_hrtime(); + + USE(wrap->DefineOwnProperty( + env()->context(), + env()->stats_string(), + stats_buffer_.GetJSArray(), + PropertyAttribute::ReadOnly)); + // TODO(@jasnell): memory accounting // env_->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); } @@ -759,8 +780,44 @@ QuicSession::~QuicSession() { CHECK(destroyed_); ssl_.reset(); ngtcp2_conn_del(connection_); -} + uint64_t now = uv_hrtime(); + Debug(this, + "Quic%sSession destroyed.\n" + " Duration: %llu\n" + " Handshake Started: %llu\n" + " Handshake Completed: %llu\n" + " Bytes Received: %llu\n" + " Bytes Sent: %llu\n" + " Bidi Stream Count: %llu\n" + " Uni Stream Count: %llu\n" + " Streams In Count: %llu\n" + " Streams Out Count: %llu\n", + IsServer() ? "Server" : "Client", + now - session_stats_.created_at, + session_stats_.handshake_start_at, + session_stats_.handshake_completed_at, + session_stats_.bytes_received, + session_stats_.bytes_sent, + session_stats_.bidi_stream_count, + session_stats_.uni_stream_count, + session_stats_.streams_in_count, + session_stats_.streams_out_count); +} + +QuicError QuicSession::GetLastError() { + return last_error_; +} + +const std::string& QuicSession::GetALPN() { + return alpn_; +} + +// TLS Keylogging is enabled per-QuicSession by attaching an handler to the +// "keylog" event. Each keylog line is emitted to JavaScript where it can +// be routed to whatever destination makes sense. Typically, this will be +// to a keylog file that can be consumed by tools like Wireshark to intercept +// and decrypt QUIC network traffic. void QuicSession::Keylog(const char* line) { if (LIKELY(state_[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] == 0)) return; @@ -789,11 +846,16 @@ void QuicSession::AckedCryptoOffset( ngtcp2_crypto_level crypto_level, uint64_t offset, size_t datalen) { - CHECK(!IsDestroyed()); - Debug(this, - "Received acknowledgement for crypto data. Offset %llu, Length %d", - offset, datalen); + if (IsDestroyed()) + return; + Debug(this, "Received acknowledgement for %d bytes of crypto data.", datalen); handshake_.Consume(datalen); + // TODO(@jasnell): Check session_stats_.handshake_send_at to see how long + // handshake ack has taken. We will want to guard against Slow Handshake + // as a DOS vector. + // Likewise, we need to guard against malicious acknowledgements that trickle + // in acknowledgements with small datalen values. These could cause the + // session to retain memory and/or send extraneous retransmissions. } // Because of the fire-and-forget nature of UDP, the QuicSession must retain @@ -801,18 +863,23 @@ void QuicSession::AckedCryptoOffset( // This applies to TLS Handshake data as well as stream data. Once acknowledged, // the buffered data can be released. This function is called only by the // OnAckedStreamDataOffset ngtcp2 callback function. -int QuicSession::AckedStreamDataOffset( +void QuicSession::AckedStreamDataOffset( int64_t stream_id, uint64_t offset, size_t datalen) { - CHECK(!IsDestroyed()); - Debug(this, - "Received acknowledgement for stream %llu data. Offset %llu, Length %d", - stream_id, offset, datalen); + if (IsDestroyed()) + return; + Debug(this, "Received acknowledgement for %d bytes of stream %llu data", + datalen, stream_id); QuicStream* stream = FindStream(stream_id); if (stream != nullptr) stream->AckedDataOffset(offset, datalen); - return 0; + // TODO(@jasnell): Check session_stats_.session_sent_at to see how long + // stream data ack has taken. We will want to guard against Slow Ack as a + // DOS vector. + // Likewise, we need to guard against malicious acknowledgements that trickle + // in acknowledgements with small datalen values. These could cause the + // session to retain memory and/or send extraneous retransmissions. } // Add the given QuicStream to this QuicSession's collection of streams. All @@ -822,19 +889,47 @@ void QuicSession::AddStream(QuicStream* stream) { CHECK(!IsClosing()); Debug(this, "Adding stream %llu to session.", stream->GetID()); streams_.emplace(stream->GetID(), stream); + + switch (stream->GetOrigin()) { + case QuicStream::QuicStreamOrigin::QUIC_STREAM_CLIENT: + if (IsServer()) + IncrementStat(1, &session_stats_, &session_stats::streams_in_count); + else + IncrementStat(1, &session_stats_, &session_stats::streams_out_count); + break; + case QuicStream::QuicStreamOrigin::QUIC_STREAM_SERVER: + if (IsServer()) + IncrementStat(1, &session_stats_, &session_stats::streams_out_count); + else + IncrementStat(1, &session_stats_, &session_stats::streams_in_count); + } + IncrementStat(1, &session_stats_, &session_stats::streams_out_count); + switch (stream->GetDirection()) { + case QuicStream::QuicStreamDirection::QUIC_STREAM_BIRECTIONAL: + IncrementStat(1, &session_stats_, &session_stats::bidi_stream_count); + break; + case QuicStream::QuicStreamDirection::QUIC_STREAM_UNIDIRECTIONAL: + IncrementStat(1, &session_stats_, &session_stats::uni_stream_count); + break; + } } -// Forwards detailed debugging information from ngtcp2. +void QuicSession::ExtendMaxStreamData( + int64_t stream_id, + uint64_t max_data) { + // TODO(@jasnell): Extend max stream data +} + +// Forwards detailed(verbose) debugging information from ngtcp2. Enabled using +// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category. void QuicSession::DebugLog( void* user_data, const char* fmt, ...) { QuicSession* session = static_cast(user_data); - char message[1024]; va_list ap; va_start(ap, fmt); - vsnprintf(message, sizeof(message), fmt, ap); + Debug(session->env(), DebugCategory::NGTCP2_DEBUG, fmt, ap); va_end(ap); - Debug(session, message); } // Destroy the QuicSession and free it. The QuicSession @@ -851,8 +946,7 @@ void QuicSession::Destroy() { // The first step is to transmit a CONNECTION_CLOSE to the connected peer. // This is going to be fire-and-forget because we're not going to wait // around for it to be received. - // TODO(@jasnell): Error code... - SendConnectionClose(0); + SendConnectionClose(); // Hold on to a reference until the function exits // so the instance is not prematurely deleted when @@ -1045,6 +1139,7 @@ int QuicSession::PathValidation( // QuicSession is destroyed. void QuicSession::Closing() { closing_ = true; + session_stats_.closing_at = uv_hrtime(); } // Copies the local transport params into the given struct @@ -1062,7 +1157,12 @@ uint32_t QuicSession::GetNegotiatedVersion() { return ngtcp2_conn_get_negotiated_version(connection_); } -// Generates and associates a new connection ID for this QuicSession +// Generates and associates a new connection ID for this QuicSession. +// ngtcp2 will call this multiple times at the start of a new connection +// in order to build a pool of available CIDs. +// TODO(@jasnell): It's possible that we could improve performance by +// generating a large pool of random data to use for CIDs when the +// session is created, then simply creating slices off that here. int QuicSession::GetNewConnectionID( ngtcp2_cid* cid, uint8_t* token, @@ -1079,7 +1179,7 @@ int QuicSession::GetNewConnectionID( return 0; } -// Returns the associated peers address. Note that this +// Returns the associated peer's address. Note that this // value can change over the lifetime of the QuicSession. // The fact that the session is not tied intrinsically to // a single address is one of the benefits of QUIC. @@ -1099,6 +1199,9 @@ void QuicSession::InitTLS() { SSL_set_msg_callback(ssl(), MessageCB); SSL_set_msg_callback_arg(ssl(), this); SSL_set_key_callback(ssl(), KeyCB, this); + SSL_set_cert_cb(ssl(), CertCB, this); + // The verification may be overriden in InitTLS_Post + SSL_set_verify(ssl(), SSL_VERIFY_NONE, crypto::VerifyCallback); // Servers and Clients do slightly different things at // this point. Both QuicClientSession and QuicServerSession @@ -1121,6 +1224,11 @@ bool QuicSession::IsInClosingPeriod() { return ngtcp2_conn_is_in_closing_period(connection_); } +bool QuicSession::IsInDrainingPeriod() { + CHECK(!IsDestroyed()); + return ngtcp2_conn_is_in_draining_period(connection_); +} + void QuicSession::OnIdleTimeout( uv_timer_t* timer) { QuicSession* session = static_cast(timer->data); @@ -1169,6 +1277,8 @@ int QuicSession::ReceiveClientInitial(const ngtcp2_cid* dcid) { // determines that the TLS Handshake is done. The only thing we // need to do at this point is let the javascript side know. void QuicSession::HandshakeCompleted() { + session_stats_.handshake_completed_at = uv_hrtime(); + SetLocalCryptoLevel(NGTCP2_CRYPTO_LEVEL_APP); HandleScope scope(env()->isolate()); Local context = env()->context(); @@ -1238,11 +1348,12 @@ int QuicSession::DoHandshakeWriteOnce() { max_pktlen_, uv_hrtime()); if (nwrite <= 0) - return 0; + return nwrite; data.Realloc(nwrite); sendbuf_.Push(std::move(data)); + session_stats_.handshake_send_at = uv_hrtime(); return SendPacket(); } @@ -1252,14 +1363,13 @@ int QuicSession::DoHandshakeReadOnce( const uint8_t* data, size_t datalen) { if (datalen > 0) { - int err = ngtcp2_conn_read_handshake( - connection_, - path, - data, - datalen, - uv_hrtime()); - if (err != 0) - return err; + RETURN_RET_IF_FAIL( + ngtcp2_conn_read_handshake( + connection_, + path, + data, + datalen, + uv_hrtime()), 0); } return 0; } @@ -1274,13 +1384,9 @@ int QuicSession::ReceiveCryptoData( size_t datalen) { CHECK(!IsDestroyed()); Debug(this, "Receiving %d bytes of crypto data.", datalen); - int err = WritePeerHandshake(crypto_level, data, datalen); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(WritePeerHandshake(crypto_level, data, datalen), 0); if (!IsHandshakeCompleted()) { - err = TLSHandshake(); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(TLSHandshake(), 0); return 0; } // It's possible that not all of the data was consumed. Anything @@ -1298,21 +1404,19 @@ const ngtcp2_cid* QuicSession::scid() const { int QuicSession::ReceiveStreamData( int64_t stream_id, int fin, - uint64_t offset, const uint8_t* data, size_t datalen) { + // TODO(@jasnell): Check datalen to guard against Slow Send DOS attack. A + // malicious sender may transmit very small packets very slowly to force + // us to keep a stream open for longer. CHECK(!IsDestroyed()); - // Locate the QuicStream to receive this data. If - // one does not exist, create it and notify the JS side... - // then pass on the received data HandleScope scope(env()->isolate()); Local context = env()->context(); Context::Scope context_scope(context); + QuicStream* stream = FindStream(stream_id); if (stream == nullptr) { if (IsClosing()) { - // If the QuicSession is closing, reject and shutdown any - // new streams that are received from the peer. return ngtcp2_conn_shutdown_stream( connection_, stream_id, @@ -1320,23 +1424,18 @@ int QuicSession::ReceiveStreamData( } stream = CreateStream(stream_id); } + CHECK_NOT_NULL(stream); + stream->ReceiveData(fin, data, datalen); - ngtcp2_conn_extend_max_stream_offset( - connection_, - stream_id, - datalen); - ngtcp2_conn_extend_max_offset( - connection_, - datalen); - - if (stream->ReceiveData(fin, data, datalen) != 0) - return -1; - - StartIdleTimer(-1); + ngtcp2_conn_extend_max_offset(connection_, datalen); return 0; } +void QuicSession::ExtendStreamOffset(QuicStream* stream, size_t amount) { + ngtcp2_conn_extend_max_stream_offset(connection_, stream->GetID(), amount); +} + // Removes the given connection id from the QuicSession. void QuicSession::RemoveConnectionID( const ngtcp2_cid* cid) { @@ -1356,6 +1455,27 @@ void QuicSession::RemoveStream( streams_.erase(stream_id); } +// Write any packets current pending for the ngtcp2 connection +int QuicSession::WritePackets() { + QuicPathStorage path; + for ( ;; ) { + MallocedBuffer data(max_pktlen_); + ssize_t nwrite = + ngtcp2_conn_write_pkt( + connection_, + &path.path, + data.data, + max_pktlen_, + uv_hrtime()); + if (nwrite <= 0) + return nwrite; + data.Realloc(nwrite); + remote_address_.Update(&path.path.remote); + sendbuf_.Push(std::move(data)); + RETURN_RET_IF_FAIL(SendPacket(), 0); + } +} + namespace { void Consume(ngtcp2_vec** pvec, size_t* pcnt, size_t len) { ngtcp2_vec* v = *pvec; @@ -1384,15 +1504,12 @@ int Empty(const ngtcp2_vec* vec, size_t cnt) { // Sends 0RTT stream data. int QuicSession::Send0RTTStreamData( QuicStream* stream, - int fin, - QuicBuffer* data, QuicBuffer::drain_from from) { CHECK(!IsDestroyed()); ssize_t ndatalen = 0; - int err; std::vector vec; - size_t count = data->DrainInto(&vec, from); + size_t count = stream->DrainInto(&vec, from); size_t c = count; ngtcp2_vec* v = vec.data(); @@ -1404,7 +1521,7 @@ int QuicSession::Send0RTTStreamData( max_pktlen_, &ndatalen, stream->GetID(), - fin, + stream->IsWritable() ? 0 : 1, reinterpret_cast(v), c, uv_hrtime()); @@ -1421,7 +1538,8 @@ int QuicSession::Send0RTTStreamData( } if (should_break) break; - return HandleError(nwrite); + SetLastError(QUIC_ERROR_CRYPTO, nwrite); + return HandleError(); } if (nwrite == 0) @@ -1433,15 +1551,13 @@ int QuicSession::Send0RTTStreamData( dest.Realloc(nwrite); sendbuf_.Push(std::move(dest)); - err = SendPacket(); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(SendPacket(), 0); if (Empty(v, c)) break; } // Advance the read head of the source buffer - data->SeekHead(count); + stream->Commit(count); return 0; } @@ -1449,19 +1565,19 @@ int QuicSession::Send0RTTStreamData( // Sends buffered stream data. int QuicSession::SendStreamData( QuicStream* stream, - int should_send_fin, - QuicBuffer* data, QuicBuffer::drain_from from) { CHECK(!IsDestroyed()); ssize_t ndatalen = 0; QuicPathStorage path; - int err; std::vector vec; - size_t count = data->DrainInto(&vec, from); - size_t c = count; + size_t count = stream->DrainInto(&vec, from); + + size_t c = vec.size(); ngtcp2_vec* v = vec.data(); + // Event if there's no data to write, we iterate through just in case + // ngtcp2 has other frames that it needs to encode. for (;;) { MallocedBuffer dest(max_pktlen_); ssize_t nwrite = @@ -1472,24 +1588,20 @@ int QuicSession::SendStreamData( max_pktlen_, &ndatalen, stream->GetID(), - should_send_fin, + stream->IsWritable() ? 0 : 1, reinterpret_cast(v), c, uv_hrtime()); if (nwrite < 0) { - auto should_break = false; - switch (nwrite) { - case NGTCP2_ERR_STREAM_DATA_BLOCKED: - case NGTCP2_ERR_EARLY_DATA_REJECTED: - case NGTCP2_ERR_STREAM_SHUT_WR: - case NGTCP2_ERR_STREAM_NOT_FOUND: - should_break = true; - break; - } - if (should_break) + if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED || + nwrite == NGTCP2_ERR_EARLY_DATA_REJECTED || + nwrite == NGTCP2_ERR_STREAM_SHUT_WR || + nwrite == NGTCP2_ERR_STREAM_NOT_FOUND) { break; - return HandleError(nwrite); + } + SetLastError(QUIC_ERROR_SESSION, nwrite); + return HandleError(); } if (nwrite == 0) @@ -1502,28 +1614,32 @@ int QuicSession::SendStreamData( sendbuf_.Push(std::move(dest)); remote_address_.Update(&path.path.remote); - err = SendPacket(); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(SendPacket(), 0); if (Empty(v, c)) break; } // Advance the read head of the source buffer - data->SeekHead(count); + stream->Commit(count); return 0; } -// Transmits the current contents of the internal sendbuf to the peer -// By default, SendPacket will drain from the txbuf_ read head. +// Transmits the current contents of the internal sendbuf_ to the peer +// By default, SendPacket will drain from the txbuf_ read head. If +// retransmit is true, the entire contents of txbuf_ will be drained. int QuicSession::SendPacket(bool retransmit) { CHECK(!IsDestroyed()); // Move the contents of sendbuf_ to the tail of txbuf_ and reset sendbuf_ - if (sendbuf_.Length() > 0) + if (sendbuf_.Length() > 0) { + IncrementStat( + sendbuf_.Length(), + &session_stats_, + &session_stats::bytes_sent); *txbuf_ += std::move(sendbuf_); - // Then pass the txbuf_ off to the socket for transmission - Debug(this, "There are %llu bytes in txbuf_", txbuf_->Length()); + } + Debug(this, "There are %llu bytes in txbuf_ to send", txbuf_->Length()); + session_stats_.session_sent_at = uv_hrtime(); return Socket()->SendPacket( &remote_address_, txbuf_, @@ -1558,14 +1674,15 @@ void QuicSession::SetLocalAddress(const ngtcp2_addr* addr) { } void QuicSession::SetTLSAlert(int err) { - tls_alert_ = err; + SetLastError(InitQuicError(QUIC_ERROR_CRYPTO, err)); } // Creates a new stream object and passes it off to the javascript side. +// This has to be called from within a handlescope/contextscope. QuicStream* QuicSession::CreateStream(int64_t stream_id) { CHECK(!IsDestroyed()); CHECK(!IsClosing()); - Debug(this, "Stream %llu is new. Creating.", stream_id); + Debug(this, "Create new stream %llu", stream_id); QuicStream* stream = QuicStream::New(this, stream_id); CHECK_NOT_NULL(stream); Local argv[] = { @@ -1578,6 +1695,10 @@ QuicStream* QuicSession::CreateStream(int64_t stream_id) { // Called by ngtcp2 when a stream has been opened. If the stream has already // been created, return an error. +// TODO(@jasnell): Currently, this will cause the stream object to be +// created, but we might want to wait to create the stream object until +// we receive the first packet of data for the stream... doing so ensures +// that we are not committing resources until we actually need to. int QuicSession::StreamOpen(int64_t stream_id) { CHECK(!IsDestroyed()); if (IsClosing()) { @@ -1597,7 +1718,9 @@ int QuicSession::StreamOpen(int64_t stream_id) { return 0; } -// Called by ngtcp2 when a strema has been reset. +// Called by ngtcp2 when a stream has been reset. Resetting a streams +// allows it's state to be completely reset for the purposes of canceling +// transmission of stream data. void QuicSession::StreamReset( int64_t stream_id, uint64_t final_size, @@ -1627,7 +1750,9 @@ int QuicSession::ShutdownStreamWrite(int64_t stream_id, uint16_t code) { int QuicSession::OpenUnidirectionalStream(int64_t* stream_id) { CHECK(!IsDestroyed()); CHECK(!IsClosing()); - return ngtcp2_conn_open_uni_stream(connection_, stream_id, nullptr); + int err = ngtcp2_conn_open_uni_stream(connection_, stream_id, nullptr); + ngtcp2_conn_shutdown_stream_read(connection_, *stream_id, 0); + return err; } int QuicSession::OpenBidirectionalStream(int64_t* stream_id) { @@ -1685,9 +1810,10 @@ void QuicSession::StopRetransmitTimer() { // Called by ngtcp2 when a stream has been closed. If the stream does // not exist, the close is ignored. void QuicSession::StreamClose(int64_t stream_id, uint16_t app_error_code) { - CHECK(!IsDestroyed()); - Debug(this, "Closing stream %llu with code %d", - stream_id, app_error_code); + // Ignore if the session has already been destroyed + if (IsDestroyed()) + return; + Debug(this, "Closing stream %llu with code %d", stream_id, app_error_code); QuicStream* stream = FindStream(stream_id); if (stream != nullptr) stream->Close(app_error_code); @@ -1699,40 +1825,26 @@ void QuicSession::StreamClose(int64_t stream_id, uint16_t app_error_code) { int QuicSession::TLSHandshake() { CHECK(!IsDestroyed()); Debug(this, "TLS handshake %s", initial_ ? "starting" : "continuing"); - ClearTLSError(); - int err; if (initial_) { - err = TLSHandshake_Initial(); - if (err != 0) { - return err; - } + session_stats_.handshake_start_at = uv_hrtime(); + } else { + // TODO(@jasnell): Check handshake_continue_at to guard against slow + // handshake attack } + session_stats_.handshake_continue_at = uv_hrtime(); + ClearTLSError(); - err = SSL_do_handshake(ssl()); - if (err <= 0) { - err = SSL_get_error(ssl(), err); - switch (err) { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - return 0; - case SSL_ERROR_SSL: - Debug(this, "TLS handshake error: %s", TLSErrorString(err)); - return NGTCP2_ERR_CRYPTO; - default: - Debug(this, "TLS handshake error: %d", err); - return NGTCP2_ERR_CRYPTO; - } + if (initial_) + RETURN_RET_IF_FAIL(TLSHandshake_Initial(), 0); + int err = DoTLSHandshake(ssl()); + if (err > 0) { + RETURN_RET_IF_FAIL(TLSHandshake_Complete(), 0); + Debug(this, "TLS Handshake completed."); + SetHandshakeCompleted(); + err = 0; } - err = TLSHandshake_Complete(); - if (err != 0) { - return err; - } - - Debug(this, "TLS Handshake completed."); - - SetHandshakeCompleted(); - return 0; + return err; } // It's possible for TLS handshake to contain extra data that is not @@ -1741,31 +1853,7 @@ int QuicSession::TLSHandshake() { int QuicServerSession::TLSRead() { CHECK(!IsDestroyed()); ClearTLSError(); - - std::array buf; - size_t nread; - Debug(this, "Reading TLS data"); - for (;;) { - int err = SSL_read_ex(ssl(), buf.data(), buf.size(), &nread); - if (err == 1) { - return NGTCP2_ERR_PROTO; - } - int code = SSL_get_error(ssl(), 0); - switch (code) { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - return 0; - case SSL_ERROR_SSL: - case SSL_ERROR_ZERO_RETURN: - // TLS read error - // std::cerr << "TLS read error: " - // << ERR_error_string(ERR_get_error(), nullptr) << std::endl; - return NGTCP2_ERR_CRYPTO; - default: - // std::cerr << "TLS read error: " << err << std::endl; - return NGTCP2_ERR_CRYPTO; - } - } + return ClearTLS(ssl()); } // Called by ngtcp2 when the QuicSession keys need to be updated. This may @@ -1773,13 +1861,12 @@ int QuicServerSession::TLSRead() { int QuicSession::UpdateKey() { CHECK(!IsDestroyed()); Debug(this, "Updating keys."); - int err; std::array secret; ssize_t secretlen; CryptoParams params; - ++nkey_update_; + IncrementStat(1, &session_stats_, &session_stats::keyupdate_count); secretlen = UpdateTrafficSecret( @@ -1815,14 +1902,13 @@ int QuicSession::UpdateKey() { if (params.ivlen < 0) return -1; - err = ngtcp2_conn_update_tx_key( - connection_, - params.key.data(), - params.keylen, - params.iv.data(), - params.ivlen); - if (err != 0) - return -1; + RETURN_IF_FAIL( + ngtcp2_conn_update_tx_key( + connection_, + params.key.data(), + params.keylen, + params.iv.data(), + params.ivlen), 0, -1); secretlen = UpdateTrafficSecret( @@ -1858,14 +1944,13 @@ int QuicSession::UpdateKey() { if (params.ivlen < 0) return -1; - err = ngtcp2_conn_update_rx_key( - connection_, - params.key.data(), - params.keylen, - params.iv.data(), - params.ivlen); - if (err != 0) - return -1; + RETURN_IF_FAIL( + ngtcp2_conn_update_rx_key( + connection_, + params.key.data(), + params.keylen, + params.iv.data(), + params.ivlen), 0, -1); return 0; } @@ -1897,13 +1982,120 @@ void QuicSession::WriteHandshake(const uint8_t* data, size_t datalen) { } // Called when the QuicSession is closed and we need to let the javascript -// side know +// side know. The error may be either a QUIC connection error code or an +// application error code, with the type differentiating between the two. +// The default type is QUIC_CLOSE_CONNECTION. void QuicSession::Close() { CHECK(!IsDestroyed()); HandleScope scope(env()->isolate()); Local context = env()->context(); Context::Scope context_scope(context); - MakeCallback(env()->quic_on_session_close_function(), 0, nullptr); + QuicError last_error = GetLastError(); + Local argv[] = { + Integer::New(env()->isolate(), last_error.code), + Integer::New(env()->isolate(), last_error.family) + }; + MakeCallback(env()->quic_on_session_close_function(), arraysize(argv), argv); +} + +namespace { +void OnServerClientHelloCB(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->OnClientHelloDone(); +} +} // namespace + +void QuicServerSession::OnClientHelloDone() { + // Continue the TLS handshake when this function exits + // otherwise it will stall and fail. + TLSHandshakeScope handshake_scope(this, &client_hello_cb_running_); + // Disable the callback at this point so we don't loop continuously + state_[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 0; +} + +// If a 'clientHello' event listener is registered on the JavaScript +// QuicServerSession object, the STATE_CLIENT_HELLO_ENABLED state +// will be set and the OnClientHello will cause the 'clientHello' +// event to be emitted. +// +// The 'clientHello' callback will be given it's own callback function +// that must be called when the client has completed handling the event. +// The handshake will not continue until it is called. +// +// The intent here is to allow user code the ability to modify or +// replace the SecurityContext based on the server name, ALPN, or +// other handshake characteristics. +// +// The user can also set a 'cert' event handler that will be called +// when the peer certificate is received, allowing additional tweaks +// and verifications to be performed. +int QuicServerSession::OnClientHello() { + if (LIKELY(state_[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] == 0)) + return 0; + + TLSHandshakeCallbackScope callback_scope(this); + + // Not an error but does suspend the handshake until we're ready to go. + // A callback function is passed to the JavaScript function below that + // must be called in order to set client_hello_cb_running_ to false. + // Once that callback is invoked, the TLS Handshake will resume. + // It is recommended that the user not take a long time to invoke the + // callback in order to avoid stalling out the QUIC connection. + if (client_hello_cb_running_) + return -1; + + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + client_hello_cb_running_ = true; + + const char* server_name; + const char* alpn; + int* exts; + size_t len; + SSL_client_hello_get1_extensions_present(ssl(), &exts, &len); + for (size_t n = 0; n < len; n++) { + switch (exts[n]) { + case TLSEXT_TYPE_server_name: + server_name = GetClientHelloServerName(ssl()); + break; + case TLSEXT_TYPE_application_layer_protocol_negotiation: + alpn = GetClientHelloALPN(ssl()); + break; + } + } + + Local argv[] = { + Undefined(env()->isolate()), + Undefined(env()->isolate()), + GetClientHelloCiphers(env(), ssl()), + Function::New( + env()->context(), + OnServerClientHelloCB, + object(), 0, + v8::ConstructorBehavior::kThrow, + v8::SideEffectType::kHasNoSideEffect).ToLocalChecked() + }; + + if (alpn != nullptr) { + argv[0] = String::NewFromUtf8( + env()->isolate(), + alpn, + v8::NewStringType::kNormal).ToLocalChecked(); + } + if (server_name != nullptr) { + argv[1] = String::NewFromUtf8( + env()->isolate(), + server_name, + v8::NewStringType::kNormal).ToLocalChecked(); + } + + MakeCallback( + env()->quic_on_session_client_hello_function(), + arraysize(argv), argv); + + OPENSSL_free(exts); + return client_hello_cb_running_ ? -1 : 0; } // The QuicServerSession specializes the QuicSession with server specific @@ -1918,14 +2110,21 @@ QuicServerSession::QuicServerSession( const struct sockaddr* addr, const ngtcp2_cid* dcid, const ngtcp2_cid* ocid, - uint32_t version) : - QuicSession(socket, - wrap, - socket->GetServerSecureContext(), - AsyncWrap::PROVIDER_QUICSERVERSESSION), + uint32_t version, + const std::string& alpn, + bool reject_unauthorized, + bool request_cert) : + QuicSession( + socket, + wrap, + socket->GetServerSecureContext(), + AsyncWrap::PROVIDER_QUICSERVERSESSION, + alpn), pscid_{}, rcid_(*rcid), - draining_(false) { + draining_(false), + reject_unauthorized_(reject_unauthorized), + request_cert_(request_cert) { Init(addr, dcid, ocid, version); } @@ -1940,16 +2139,11 @@ int QuicServerSession::DoHandshake( const uint8_t* data, size_t datalen) { CHECK(!IsDestroyed()); - RETURN_IF_FAIL(DoHandshakeReadOnce(path, data, datalen), 0, -1); - - int err = SendPacket(); - if (err != 0) - return err; - + RETURN_RET_IF_FAIL(SendPacket(), 0); + ssize_t nwrite; for (;;) { - ssize_t nwrite = DoHandshakeWriteOnce(); - if (nwrite <= 0) + if ((nwrite = DoHandshakeWriteOnce()) <= 0) return nwrite; } } @@ -1966,9 +2160,148 @@ void QuicServerSession::AddToSocket(QuicSocket* socket) { } } -int QuicServerSession::HandleError( - int error) { - return SendConnectionClose(error); +int QuicServerSession::HandleError() { + if (!SendConnectionClose()) { + SetLastError(QUIC_ERROR_SESSION, NGTCP2_ERR_INTERNAL); + Close(); + } + return 0; +} + +namespace { +// This callback is invoked by user code after completing handling +// of the 'OCSPRequest' event. The callback is invoked with two +// possible arguments, both of which are optional +// 1. A replacement SecureContext +// 2. An OCSP response +void OnServerCertCB(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicServerSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + Local cons = env->secure_context_constructor_template(); + crypto::SecureContext* context = nullptr; + if (args[0]->IsObject() && cons->HasInstance(args[0])) + ASSIGN_OR_RETURN_UNWRAP(&context, args[0].As()); + session->OnCertDone(context, args[1]); +} +} // namespace + +// The OnCertDone function is called by the OnServerCertCB +// function when usercode is done handling the OCSPRequest event. +void QuicServerSession::OnCertDone( + crypto::SecureContext* context, + Local ocsp_response) { + Debug(this, "OCSPRequest completed. Context Provided? %s, OCSP Provided? %s", + context != nullptr ? "Yes" : "No", + ocsp_response->IsArrayBufferView() ? "Yes" : "No"); + // Continue the TLS handshake when this function exits + // otherwise it will stall and fail. + TLSHandshakeScope handshake_scope(this, &cert_cb_running_); + // Disable the callback at this point so we don't loop continuously + state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 0; + + if (context != nullptr) { + int err = UseSNIContext(ssl(), context); + if (!err) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + if (!err) + return env()->ThrowError("CertCbDone"); // TODO(@jasnell): revisit + return crypto::ThrowCryptoError(env(), err); + } + } + + if (ocsp_response->IsArrayBufferView()) + ocsp_response_.Reset(env()->isolate(), ocsp_response.As()); +} + +// The OnCert callback provides an opportunity to prompt the server to +// perform on OCSP request on behalf of the client (when the client +// requests it). If there is a listener for the 'OCSPRequest' event +// on the JavaScript side, the IDX_QUIC_SESSION_STATE_CERT_ENABLED +// session state slot will equal 1, which will cause the callback to +// be invoked. The callback will be given a reference to a JavaScript +// function that must be called in order for the TLS handshake to +// continue. +int QuicServerSession::OnCert() { + Debug(this, "Is there an OCSPRequest handler registered? %s", + state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] == 0 ? "No" : "Yes"); + if (LIKELY(state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] == 0)) + return 1; + + TLSHandshakeCallbackScope callback_scope(this); + + // As in node_crypto.cc, this is not an error, but does suspend the + // handshake to continue when OnCerb is complete. + if (cert_cb_running_) + return -1; + + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + Local servername_str; + const bool ocsp = + (SSL_get_tlsext_status_type(ssl()) == TLSEXT_STATUSTYPE_ocsp); + Debug(this, "Is the client requesting OCSP? %s", ocsp ? "Yes" : "No"); + + // If status type is not ocsp, there's nothing further to do here. + // Save ourselves the callback into JavaScript and continue the + // handshake. + if (!ocsp) + return 1; + + const char* servername = SSL_get_servername(ssl(), TLSEXT_NAMETYPE_host_name); + + cert_cb_running_ = true; + Local argv[] = { + servername == nullptr ? + String::Empty(env()->isolate()) : + OneByteString( + env()->isolate(), + servername, + strlen(servername)), + Function::New( + env()->context(), + OnServerCertCB, + object(), 0, + v8::ConstructorBehavior::kThrow, + v8::SideEffectType::kHasNoSideEffect).ToLocalChecked() + }; + + MakeCallback(env()->quic_on_session_cert_function(), arraysize(argv), argv); + + return cert_cb_running_ ? -1 : 1; +} + +// When the client has requested OSCP, this function will be called to provide +// the OSCP response. The OnCert() callback should have already been called +// by this point if any data is to be provided. If it hasn't, and ocsp_response_ +// is empty, no OCSP response will be sent. +int QuicServerSession::OnTLSStatus() { + Debug(this, "Asking for OCSP status to send. Is there a response? %s", + ocsp_response_.IsEmpty() ? "No" : "Yes"); + + if (ocsp_response_.IsEmpty()) + return SSL_TLSEXT_ERR_NOACK; + + HandleScope scope(env()->isolate()); + + Local obj = + PersistentToLocal::Default( + env()->isolate(), + ocsp_response_); + size_t len = obj->ByteLength(); + + unsigned char* data = crypto::MallocOpenSSL(len); + obj->CopyContents(data, len); + + Debug(this, "The OCSP Response is %d bytes in length.", len); + + if (!SSL_set_tlsext_status_ocsp_resp(ssl(), data, len)) + OPENSSL_free(data); + ocsp_response_.Reset(); + + return SSL_TLSEXT_ERR_OK; } int QuicServerSession::OnKey( @@ -2030,6 +2363,13 @@ int QuicServerSession::OnKey( void QuicServerSession::InitTLS_Post() { SSL_set_accept_state(ssl()); + + if (request_cert_) { + int verify_mode = SSL_VERIFY_PEER; + if (reject_unauthorized_) + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_set_verify(ssl(), verify_mode, crypto::VerifyCallback); + } } void QuicServerSession::Init( @@ -2081,7 +2421,10 @@ std::shared_ptr QuicServerSession::New( const struct sockaddr* addr, const ngtcp2_cid* dcid, const ngtcp2_cid* ocid, - uint32_t version) { + uint32_t version, + const std::string& alpn, + bool reject_unauthorized, + bool request_cert) { std::shared_ptr session; Local obj; if (!socket->env() @@ -2097,7 +2440,10 @@ std::shared_ptr QuicServerSession::New( addr, dcid, ocid, - version)); + version, + alpn, + reject_unauthorized, + request_cert)); session->AddToSocket(socket); return session; @@ -2142,56 +2488,65 @@ int QuicServerSession::Receive( const struct sockaddr* addr, unsigned int flags) { CHECK(!IsDestroyed()); - Debug(this, "Received packet. nread = %d bytes", nread); + + SendScope scope(this); + int err; + IncrementStat(nread, &session_stats_, &session_stats::bytes_received); // Closing period starts once ngtcp2 has detected that the session - // is being shutdown locally. + // is being shutdown locally. Note that this is different that the + // IsClosing() function, which indicates a graceful shutdown that + // allows the session and streams to finish naturally. When + // IsInClosingPeriod is true, ngtcp2 is actively in the process + // of shutting down the connection and a CONNECTION_CLOSE has + // already been sent. The only thing we can do at this point is + // either ignore the packet or send another CONNECTION_CLOSE. + // + // TODO(@jasnell): Currently, send a CONNECTION_CLOSE on every + // packet received. To be a bit nicer, however, we could + // use an exponential backoff. if (IsInClosingPeriod()) { - Debug(this, "In closing period"); - // If we receive anything while we're shutting down, just repeat - // the connection close. It's possible that the peer either hasn't - // received it yet or it got lost. To be a bit nicer, we could - // implement this with an exponential backoff but this strategy - // works for now. - return SendConnectionClose(0); + SetLastError(QUIC_ERROR_SESSION, NGTCP2_ERR_CLOSING); + return HandleError(); } - // Draining period starts once we've detected and idle timeout on + // Draining period starts once we've detected an idle timeout on // this session and we're in the process of shutting down. We // don't want to accept any new packets during this time, so we // simply ignore them. - if (IsDraining()) { - Debug(this, "Draining..."); + if (IsDraining()) return 0; - } + // With QUIC, it is possible for the remote address to change + // from one packet to the next. remote_address_.Copy(addr); QuicPath path(Socket()->GetLocalAddress(), &remote_address_); - if (IsHandshakeCompleted()) { - err = ngtcp2_conn_read_pkt( - connection_, - *path, - data, nread, - uv_hrtime()); + if (!IsHandshakeCompleted()) { + err = DoHandshake(*path, data, nread); if (err != 0) { - Debug(this, "Error reading packet. Error %d\n", err); - if (err == NGTCP2_ERR_DRAINING) { - StartDrainingPeriod(); - return -1; // Closing - } - return HandleError(err); + SetLastError(InitQuicError(QUIC_ERROR_CRYPTO, err)); + HandleError(); } - Debug(this, "Successfully read packet"); return 0; } - Debug(this, "TLS Handshake %s", initial_ ? "starting" : "continuing"); - err = DoHandshake(*path, data, nread); - if (err != 0) - return HandleError(err); + uint64_t now = uv_hrtime(); + err = ngtcp2_conn_read_pkt( + connection_, + *path, + data, nread, + now); + if (err == NGTCP2_ERR_DRAINING) { + StartDrainingPeriod(); + return -1; + } else if (ngtcp2_err_is_fatal(err)) { + SetLastError(QUIC_ERROR_SESSION, err); + HandleError(); + } + session_stats_.session_received_at = now; return 0; } @@ -2222,9 +2577,9 @@ void QuicServerSession::RemoveFromSocket() { // Transmits the CONNECTION_CLOSE to the peer, signaling // the end of this QuicSession. -int QuicServerSession::SendConnectionClose(int error) { +bool QuicServerSession::SendConnectionClose() { CHECK(!IsDestroyed()); - RETURN_IF_FAIL(StartClosingPeriod(error), 0, -1); + RETURN_IF_FAIL(StartClosingPeriod(), 0, false); StartIdleTimer(-1); CHECK_GT(conn_closebuf_.size, 0); sendbuf_.Cancel(); @@ -2236,7 +2591,7 @@ int QuicServerSession::SendConnectionClose(int error) { conn_closebuf_.size); sendbuf_.Push(&buf, 1); ScheduleMonitor(); - return SendPacket(); + return SendPacket() == 0; } int QuicServerSession::SendPendingData(bool retransmit) { @@ -2246,13 +2601,14 @@ int QuicServerSession::SendPendingData(bool retransmit) { Debug(this, "Sending pending data for server session"); int err; - if (IsInClosingPeriod()) + // If we're in the process of closing or draining the connection, do nothing. + if (IsInClosingPeriod() || IsInDrainingPeriod()) return 0; - err = SendPacket(); - if (err != 0) - return err; + // If there's anything currently in the sendbuf_, send it. + RETURN_RET_IF_FAIL(SendPacket(), 0); + // If the handshake is not yet complete, perform the handshake if (!IsHandshakeCompleted()) { err = DoHandshake(nullptr, nullptr, 0); if (err == 0) @@ -2260,76 +2616,51 @@ int QuicServerSession::SendPendingData(bool retransmit) { return err; } + // For every stream, transmit the stream data, returning + // early if we're unable to send stream data for some + // reason. for (auto stream : streams_) { - err = stream.second->SendPendingData(retransmit); - if (err != 0) - return err; + RETURN_RET_IF_FAIL( + SendStreamData( + stream.second, + retransmit ? + QuicBuffer::DRAIN_FROM_ROOT : QuicBuffer::DRAIN_FROM_HEAD), 0); } - QuicPathStorage path; - - // We call ngtcp2_conn_write_pkt repeatedly until it has - // no more data to write. - for ( ;; ) { - MallocedBuffer data(max_pktlen_); - ssize_t n = - ngtcp2_conn_write_pkt( - connection_, - &path.path, - data.data, - max_pktlen_, - uv_hrtime()); - if (n < 0) { - Debug(this, "There was an error writing the packet. Error %d", n); - return HandleError(n); - } - if (n == 0) - break; - - sendbuf_.Push(std::move(data)); - remote_address_.Update(&path.path.remote); - - err = SendPacket(); - if (err != 0) { - Debug(this, "Error sending packet. Error %d", err); - return err; - } + err = WritePackets(); + if (err < 0) { + SetLastError(QUIC_ERROR_SESSION, err); + HandleError(); + return 0; } - Debug(this, "Done sending pending server session data"); ScheduleMonitor(); return 0; } -int QuicServerSession::StartClosingPeriod(int error) { +int QuicServerSession::StartClosingPeriod() { CHECK(!IsDestroyed()); if (IsInClosingPeriod()) return 0; - Debug(this, "Closing period has started. Error %d", error); - StopRetransmitTimer(); StartIdleTimer(-1); sendbuf_.Cancel(); - uint16_t err_code; - if (tls_alert_) { - err_code = NGTCP2_CRYPTO_ERROR | tls_alert_; - } else { - err_code = ngtcp2_err_infer_quic_transport_error_code(error); - } + QuicError error = GetLastError(); + Debug(this, "Closing period has started. Error %d", error.code); // Once the CONNECTION_CLOSE packet is written, // IsInClosingPeriod will return true. conn_closebuf_ = MallocedBuffer(max_pktlen_); ssize_t nwrite = - ngtcp2_conn_write_connection_close( + SelectCloseFn(error.family)( connection_, nullptr, conn_closebuf_.data, max_pktlen_, - err_code, + error.code, uv_hrtime()); if (nwrite < 0) return -1; @@ -2347,36 +2678,8 @@ void QuicServerSession::StartDrainingPeriod() { } int QuicServerSession::TLSHandshake_Initial() { - std::array buf; - size_t nread; - int err = SSL_read_early_data(ssl(), buf.data(), buf.size(), &nread); initial_ = false; - switch (err) { - case SSL_READ_EARLY_DATA_ERROR: { - Debug(this, "TLS Read Early Data Error. Error %d", err); - int code = SSL_get_error(ssl(), err); - switch (code) { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - return 0; - case SSL_ERROR_SSL: - Debug(this, "TLS handshake error: %s", TLSErrorString(code)); - return NGTCP2_ERR_CRYPTO; - default: - Debug(this, "TLS handshake error: %d", code); - return NGTCP2_ERR_CRYPTO; - } - break; - } - case SSL_READ_EARLY_DATA_SUCCESS: - Debug(this, "TLS Read Early Data Success"); - if (nread > 0) - return NGTCP2_ERR_PROTO; - break; - case SSL_READ_EARLY_DATA_FINISH: - Debug(this, "TLS Read Early Data Finish"); - } - return 0; + return DoTLSReadEarlyData(ssl()); } int QuicServerSession::TLSHandshake_Complete() { @@ -2445,7 +2748,9 @@ std::shared_ptr QuicClientSession::New( Local early_transport_params, Local session_ticket, Local dcid, - int select_preferred_address_policy) { + int select_preferred_address_policy, + const std::string& alpn, + bool request_ocsp) { std::shared_ptr session; Local obj; if (!socket->env() @@ -2466,7 +2771,9 @@ std::shared_ptr QuicClientSession::New( early_transport_params, session_ticket, dcid, - select_preferred_address_policy); + select_preferred_address_policy, + alpn, + request_ocsp); session->AddToSocket(socket); session->Start(); @@ -2485,11 +2792,20 @@ QuicClientSession::QuicClientSession( Local early_transport_params, Local session_ticket, Local dcid, - int select_preferred_address_policy) : - QuicSession(socket, wrap, context, AsyncWrap::PROVIDER_QUICCLIENTSESSION), + int select_preferred_address_policy, + const std::string& alpn, + bool request_ocsp) : + QuicSession( + socket, + wrap, + context, + AsyncWrap::PROVIDER_QUICCLIENTSESSION, + alpn), resumption_(false), hostname_(hostname), - select_preferred_address_policy_(select_preferred_address_policy) { + select_preferred_address_policy_(select_preferred_address_policy), + request_ocsp_(request_ocsp) { + // TODO(@jasnell): Init may fail. Need to handle the error conditions Init(addr, version, early_transport_params, session_ticket, dcid); } @@ -2532,7 +2848,7 @@ int QuicClientSession::Init( QuicPath path(Socket()->GetLocalAddress(), &remote_address_); - int err = + RETURN_RET_IF_FAIL( ngtcp2_conn_client_new( &connection_, &dcid, @@ -2542,29 +2858,17 @@ int QuicClientSession::Init( &callbacks_, &settings, *allocator_, - static_cast(this)); - if (err != 0) { - Debug(this, "There was an error creating the session. Error %d", err); - return err; - } + static_cast(this)), 0); - err = SetupInitialCryptoContext(); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(SetupInitialCryptoContext(), 0); // Remote Transport Params - if (early_transport_params->IsArrayBufferView()) { - err = SetEarlyTransportParams(early_transport_params); - if (err != 0) - return err; - } + if (early_transport_params->IsArrayBufferView()) + RETURN_RET_IF_FAIL(SetEarlyTransportParams(early_transport_params), 0); // Session Ticket - if (session_ticket->IsArrayBufferView()) { - err = SetSession(session_ticket); - if (err != 0) - return err; - } + if (session_ticket->IsArrayBufferView()) + RETURN_RET_IF_FAIL(SetSession(session_ticket), 0); StartIdleTimer(settings.idle_timeout); return 0; @@ -2601,12 +2905,8 @@ int QuicClientSession::SelectPreferredAddress( } int QuicClientSession::Start() { - int err; - for (auto stream : streams_) { - err = stream.second->Send0RTTData(); - if (err != 0) - return err; - } + for (auto stream : streams_) + RETURN_RET_IF_FAIL(Send0RTTStreamData(stream.second), 0); return DoHandshakeWriteOnce(); } @@ -2707,17 +3007,47 @@ int QuicClientSession::SetSession(SSL_SESSION* session) { void QuicClientSession::InitTLS_Post() { SSL_set_connect_state(ssl()); - const uint8_t* alpn = reinterpret_cast(NGTCP2_ALPN_H3); - size_t alpnlen = strsize(NGTCP2_ALPN_H3); + Debug(this, "Using %s as the ALPN protocol.", GetALPN().c_str() + 1); + const uint8_t* alpn = reinterpret_cast(GetALPN().c_str()); + size_t alpnlen = GetALPN().length(); SSL_set_alpn_protos(ssl(), alpn, alpnlen); // If the hostname is an IP address and we have no additional // information, use localhost. if (SocketAddress::numeric_host(hostname_)) { + Debug(this, "Using localhost as fallback hostname."); SSL_set_tlsext_host_name(ssl(), "localhost"); } else { SSL_set_tlsext_host_name(ssl(), hostname_); } + + // Are we going to request OCSP status? + if (request_ocsp_) { + Debug(this, "Request OCSP status from the server."); + SSL_set_tlsext_status_type(ssl(), TLSEXT_STATUSTYPE_ocsp); + } +} + +// During TLS handshake, if the client has requested OCSP status, this +// function will be invoked when the response has been received from +// the server. +int QuicClientSession::OnTLSStatus() { + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + const unsigned char* resp; + int len = SSL_get_tlsext_status_ocsp_resp(ssl(), &resp); + Debug(this, "An OCSP Response of %d bytes has been received.", len); + Local arg; + if (resp == nullptr) { + arg = Undefined(env()->isolate()); + } else { + arg = Buffer::Copy(env(), reinterpret_cast(resp), len) + .ToLocalChecked(); + } + + MakeCallback(env()->quic_on_session_status_function(), 1, &arg); + return 1; } int QuicClientSession::OnKey( @@ -2780,31 +3110,7 @@ int QuicClientSession::OnKey( int QuicClientSession::TLSRead() { CHECK(!IsDestroyed()); ClearTLSError(); - - std::array buf; - size_t nread; - Debug(this, "Reading TLS data"); - for (;;) { - int err = SSL_read_ex(ssl(), buf.data(), buf.size(), &nread); - if (err == 1) { - continue; - } - int code = SSL_get_error(ssl(), 0); - switch (code) { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - return 0; - case SSL_ERROR_SSL: - case SSL_ERROR_ZERO_RETURN: - // TLS read error - // std::cerr << "TLS read error: " - // << ERR_error_string(ERR_get_error(), nullptr) << std::endl; - return NGTCP2_ERR_CRYPTO; - default: - // std::cerr << "TLS read error: " << err << std::endl; - return NGTCP2_ERR_CRYPTO; - } - } + return ClearTLS(ssl(), true); } int QuicClientSession::DoHandshake( @@ -2814,23 +3120,18 @@ int QuicClientSession::DoHandshake( CHECK(!IsDestroyed()); - int err; - err = SendPacket(); - if (err != 0) - return err; + RETURN_RET_IF_FAIL(SendPacket(), 0); - err = DoHandshakeReadOnce(path, data, datalen); + int err = DoHandshakeReadOnce(path, data, datalen); if (err != 0) { + SetLastError(QUIC_ERROR_CRYPTO, err); Close(); return -1; } // Zero Round Trip - for (auto stream : streams_) { - err = stream.second->Send0RTTData(); - if (err != 0) - return err; - } + for (auto stream : streams_) + RETURN_RET_IF_FAIL(Send0RTTStreamData(stream.second), 0); ssize_t nwrite; for (;;) { @@ -2841,38 +3142,35 @@ int QuicClientSession::DoHandshake( return nwrite; } -int QuicClientSession::HandleError(int code) { +int QuicClientSession::HandleError() { if (!connection_ || IsInClosingPeriod()) return 0; sendbuf_.Cancel(); - if (code == NGTCP2_ERR_RECV_VERSION_NEGOTIATION) + if (GetLastError().code == NGTCP2_ERR_RECV_VERSION_NEGOTIATION) return 0; - // TODO(danbev) Use error code - /* - uint16_t err_code = - tls_alert_ ? - NGTCP2_CRYPTO_ERROR | tls_alert_ : - ngtcp2_err_infer_quic_transport_error_code(code); - */ - - return SendConnectionClose(code); + if (!SendConnectionClose()) { + SetLastError(QUIC_ERROR_SESSION, NGTCP2_ERR_INTERNAL); + Close(); + } + return 0; } -int QuicClientSession::SendConnectionClose(int error) { +bool QuicClientSession::SendConnectionClose() { CHECK(!IsDestroyed()); StartIdleTimer(-1); MallocedBuffer data(max_pktlen_); sendbuf_.Cancel(); + QuicError error = GetLastError(); ssize_t nwrite = - ngtcp2_conn_write_connection_close( + SelectCloseFn(error.family)( connection_, nullptr, data.data, max_pktlen_, - error, + error.code, uv_hrtime()); if (nwrite < 0) { Debug(this, "Error writing connection close: %d", nwrite); @@ -2881,7 +3179,7 @@ int QuicClientSession::SendConnectionClose(int error) { data.Realloc(nwrite); sendbuf_.Push(std::move(data)); ScheduleMonitor(); - return SendPacket(); + return SendPacket() == 0; } void QuicClientSession::OnIdleTimeout() { @@ -2905,13 +3203,17 @@ bool QuicClientSession::MaybeTimeout() { CHECK_EQ(ngtcp2_conn_on_loss_detection_timer(connection_, now), 0); Debug(this, "Retransmitting due to loss detection"); err = SendPendingData(true); - if (err != 0) - HandleError(err); + if (err != 0) { + SetLastError(QUIC_ERROR_SESSION, err); + HandleError(); + } } else if (ngtcp2_conn_ack_delay_expiry(connection_) <= now) { Debug(this, "Transmitting due to ack delay"); err = SendPendingData(); - if (err != 0) - HandleError(err); + if (err != 0) { + SetLastError(QUIC_ERROR_SESSION, err); + HandleError(); + } } allow_retransmit_ = false; monitor_scheduled_ = false; @@ -2925,29 +3227,30 @@ int QuicClientSession::Receive( const struct sockaddr* addr, unsigned int flags) { CHECK(!IsDestroyed()); - Debug(this, "Received packet. nread = %d bytes", nread); - int err; + SendScope scope(this); + IncrementStat(nread, &session_stats_, &session_stats::bytes_received); + + // It's possible for the remote address to change from one + // packet to the next remote_address_.Copy(addr); QuicPath path(Socket()->GetLocalAddress(), &remote_address_); - if (IsHandshakeCompleted()) { - err = ngtcp2_conn_read_pkt( - connection_, - *path, - data, nread, - uv_hrtime()); - if (err != 0) { - // TODO(@jasnell): Close with the error code? - Close(); - return err; - } - Debug(this, "Successfully read packet"); - } else { - Debug(this, "TLS Handshake continuing"); + if (!IsHandshakeCompleted()) return DoHandshake(*path, data, nread); + + uint64_t now = uv_hrtime(); + int err = ngtcp2_conn_read_pkt( + connection_, + *path, + data, nread, + now); + if (err != 0) { + Close(); + return err; } - StartIdleTimer(-1); + + session_stats_.session_received_at = now; return 0; } @@ -3003,15 +3306,15 @@ int QuicClientSession::SendPendingData(bool retransmit) { return 0; Debug(this, "Sending pending data for client session"); - int err = SendPacket(); - if (err != 0) - return err; + // First, send any data currently sitting in the sendbuf_ buffer + RETURN_RET_IF_FAIL(SendPacket(), 0); + int err; + // If we're retransmitting, reset the loss detection timer if (retransmit) { err = ngtcp2_conn_on_loss_detection_timer(connection_, uv_hrtime()); if (err != 0) { - Debug(this, "Error resetting loss detection timer. Error %d", err); - // TODO(@jasnell): Close with error code + SetLastError(QUIC_ERROR_SESSION, err); Close(); return -1; } @@ -3020,46 +3323,22 @@ int QuicClientSession::SendPendingData(bool retransmit) { if (!IsHandshakeCompleted()) { Debug(this, "Handshake is not completed"); err = DoHandshake(nullptr, nullptr, 0); - // ScheduleMonitor(); + ScheduleMonitor(); return err; } - // Call ngtcp2_conn_write_pkt repeatedly until there is no more - // data to send. - for ( ;; ) { - Debug(this, "Writing packet data"); - MallocedBuffer data(max_pktlen_); - ssize_t nwrite = - ngtcp2_conn_write_pkt( - connection_, - nullptr, - data.data, - max_pktlen_, - uv_hrtime()); - if (nwrite < 0) { - Debug(this, "There was an error writing the packet. Error %d", nwrite); - return HandleError(nwrite); - } - if (nwrite == 0) - break; - data.Realloc(nwrite); - sendbuf_.Push(std::move(data)); - - err = SendPacket(); - if (err != 0) - return err; + err = WritePackets(); + if (err < 0) { + SetLastError(QUIC_ERROR_SESSION, err); + HandleError(); } if (!retransmit) { - for (auto stream : streams_) { - err = stream.second->SendPendingData(); - if (err != 0) - return err; - } + // For each stream, send any pending data + for (auto stream : streams_) + RETURN_RET_IF_FAIL(SendStreamData(stream.second), 0); } - Debug(this, "Done sending pending client session data"); - ScheduleMonitor(); return 0; } @@ -3170,9 +3449,11 @@ void QuicSessionDestroy(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); QuicSession* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - int error_code = 0; - USE(args[0]->Int32Value(env->context()).To(&error_code)); - // Use the error code + int code = 0; + int family = QUIC_ERROR_SESSION; + USE(args[0]->Int32Value(env->context()).To(&code)); + USE(args[1]->Int32Value(env->context()).To(&family)); + session->SetLastError(static_cast(family), code); session->Destroy(); } @@ -3333,6 +3614,13 @@ void NewQuicClientSession(const FunctionCallbackInfo& args) { USE(args[10]->Int32Value( env->context()).To(&select_preferred_address_policy)); + std::string alpn(NGTCP2_ALPN_H3); + if (args[11]->IsString()) { + Utf8Value val(env->isolate(), args[11]); + alpn = val.length(); + alpn += *val; + } + socket->ReceiveStart(); std::shared_ptr session = @@ -3345,9 +3633,11 @@ void NewQuicClientSession(const FunctionCallbackInfo& args) { args[7], args[8], args[9], - select_preferred_address_policy); + select_preferred_address_policy, + alpn, + args[12]->IsTrue()); // request_oscp - socket->SendPendingData(); + session->SendPendingData(); args.GetReturnValue().Set(session->object()); } diff --git a/src/node_quic_session.h b/src/node_quic_session.h index 04cf8fa6f5..1b9ec9cb72 100644 --- a/src/node_quic_session.h +++ b/src/node_quic_session.h @@ -35,7 +35,7 @@ constexpr int ERR_INVALID_TLS_SESSION_TICKET = -2; V(MAX_STREAM_DATA_BIDI_LOCAL, max_stream_data_bidi_local, 256 * 1024) \ V(MAX_STREAM_DATA_BIDI_REMOTE, max_stream_data_bidi_remote, 256 * 1024) \ V(MAX_STREAM_DATA_UNI, max_stream_data_uni, 256 * 1024) \ - V(MAX_DATA, max_data, 1 * (1024 ^ 2)) \ + V(MAX_DATA, max_data, 1 * 1024 * 1024) \ V(MAX_STREAMS_BIDI, max_streams_bidi, 100) \ V(MAX_STREAMS_UNI, max_streams_uni, 3) \ V(IDLE_TIMEOUT, idle_timeout, 10 * 1000) \ @@ -47,18 +47,34 @@ constexpr int ERR_INVALID_TLS_SESSION_TICKET = -2; QUICSESSION_CONFIG(V) #undef V +// The QuicSessionConfig class holds the initial transport parameters and +// configuration options set by the JavaScript side when either a +// QuicClientSession or QuicServerSession is created. Instances are +// stack created and use a combination of an AliasedBuffer to pass +// the numeric settings quickly (see node_quic_state.h) and passed +// in non-numeric settings (e.g. preferred_addr). class QuicSessionConfig { public: QuicSessionConfig() = default; void ResetToDefaults(); + + // QuicSessionConfig::Set() is where the magic happens. It pulls + // values out of the AliasedBuffer defined in node_quic_state.h + // and stores the values. If the preferred_addr is set, it will + // be copied into preferred_address_. void Set( Environment* env, const struct sockaddr* preferred_addr = nullptr); + + // When a ngtcp2 connection is created, ToSettings is used to + // populate the given ngtcp2_settings object with the stored + // parameters. These are translated into QUIC transport params. void ToSettings( ngtcp2_settings* settings, ngtcp2_cid* pscid, bool stateless_reset_token = false); + size_t GetMaxCidLen() { return max_cid_len_; } size_t GetMinCidLen() { return min_cid_len_; } @@ -72,12 +88,60 @@ class QuicSessionConfig { SocketAddress preferred_address_; }; +// The QuicSessionState enums are used with the QuicSession's +// private state_ array. This is exposed to JavaScript via an +// aliased buffer and is used to communicate various types of +// state efficiently across the native/JS boundary. enum QuicSessionState { + // Communicates the number of available connection ID's that + // have been created and associated with the session. This + // is used, for instance, to enable migration of a QuicSession + // from one QuicSocket to another (when count > 0). IDX_QUIC_SESSION_STATE_CONNECTION_ID_COUNT, + + // Communicates whether a 'keylog' event listener has been + // registered on the JavaScript QuicSession object. The + // value will be either 1 or 0. When set to 1, the native + // code will emit TLS keylog entries to the JavaScript + // side triggering the 'keylog' event once for each line. IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED, + + // Communicates whether a 'clientHello' event listener has + // been registered on the JavaScript QuicServerSession. + // The value will be either 1 or 0. When set to 1, the + // native code will callout to the JavaScript side causing + // the 'clientHello' event to be emitted. This is only + // used on QuicServerSession instances. + IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED, + + // Communicates whether a 'cert' event listener has been + // registered on the JavaScript QuicSession. The value will + // be either 1 or 0. When set to 1, then native code will + // callout to the JavaScript side causing the 'cert' event + // to be emitted. + IDX_QUIC_SESSION_STATE_CERT_ENABLED, + + // Just the number of session state enums for use when + // creating the AliasedBuffer. IDX_QUIC_SESSION_STATE_COUNT }; +// The QuicSession class is an virtual class that serves as +// the basis for both QuicServerSession and QuicClientSession. +// It implements the functionality that is shared for both +// QUIC clients and servers. +// +// QUIC sessions are virtual connections that exchange data +// back and forth between peer endpoints via UDP. Every QuicSession +// has an associated TLS context and all data transfered between +// the peers is always encrypted. Unlike TLS over TCP, however, +// The QuicSession uses a session identifier that is independent +// of both the local *and* peer IP address, allowing a QuicSession +// to persist across changes in the network (one of the key features +// of QUIC). QUIC sessions also support 0RTT, implement error +// correction mechanisms to recover from lost packets, and flow +// control. In other words, there's quite a bit going on within +// a QuicSession object. class QuicSession : public AsyncWrap, public std::enable_shared_from_this, public mem::Tracker { @@ -85,16 +149,29 @@ class QuicSession : public AsyncWrap, static const int kInitialClientBufferLength = 4096; QuicSession( + // The QuicSocket that created this session. Note that + // it is possible to replace this socket later, after + // the TLS handshake has completed. The QuicSession + // should never assume that the socket will always + // remain the same. QuicSocket* socket, v8::Local wrap, crypto::SecureContext* ctx, - AsyncWrap::ProviderType provider); + AsyncWrap::ProviderType provider, + // QUIC is generally just a transport. The ALPN identifier + // is used to specify the application protocol that is + // layered on top. If not specified, this will default + // to the HTTP/3 identifier. + const std::string& alpn); ~QuicSession() override; void AddStream(QuicStream* stream); void Close(); void Closing(); void Destroy(); + void ExtendStreamOffset(QuicStream* stream, size_t amount); + const std::string& GetALPN(); + inline QuicError GetLastError(); void GetLocalTransportParams( ngtcp2_transport_params* params); uint32_t GetNegotiatedVersion(); @@ -112,21 +189,23 @@ class QuicSession : public AsyncWrap, int ReceiveStreamData( int64_t stream_id, int fin, - uint64_t offset, const uint8_t* data, size_t datalen); void RemoveStream( int64_t stream_id); int Send0RTTStreamData( QuicStream* stream, - int fin, - QuicBuffer* data, QuicBuffer::drain_from from = QuicBuffer::DRAIN_FROM_HEAD); int SendStreamData( QuicStream* stream, - int fin, - QuicBuffer* data, QuicBuffer::drain_from from = QuicBuffer::DRAIN_FROM_HEAD); + inline void SetLastError( + QuicError error = { QUIC_ERROR_SESSION, NGTCP2_NO_ERROR }) { + last_error_ = error; + } + inline void SetLastError(QuicErrorFamily family, int code) { + SetLastError(InitQuicError(family, code)); + } int SetRemoteTransportParams( ngtcp2_transport_params* params); void SetTLSAlert( @@ -147,6 +226,12 @@ class QuicSession : public AsyncWrap, // These may be implemented by QuicSession types virtual bool IsServer() const { return false; } + virtual int OnClientHello() { return 0; } + virtual void OnClientHelloDone() {} + virtual int OnCert() { return 1; } + virtual void OnCertDone( + crypto::SecureContext* context, + v8::Local ocsp_response) {} // These must be implemented by QuicSession types virtual void AddToSocket(QuicSocket* socket) = 0; @@ -154,8 +239,7 @@ class QuicSession : public AsyncWrap, const ngtcp2_path* path, const uint8_t* data, size_t datalen) = 0; - virtual int HandleError( - int code) = 0; + virtual int HandleError() = 0; virtual bool MaybeTimeout() = 0; virtual void OnIdleTimeout() = 0; virtual int OnKey( @@ -169,13 +253,13 @@ class QuicSession : public AsyncWrap, const struct sockaddr* addr, unsigned int flags) = 0; virtual void RemoveFromSocket() = 0; - virtual int SendConnectionClose( - int error) = 0; + virtual bool SendConnectionClose() = 0; virtual int SendPendingData( bool retransmit = false) = 0; virtual int TLSHandshake_Complete() = 0; virtual int TLSHandshake_Initial() = 0; virtual int TLSRead() = 0; + virtual int OnTLSStatus() = 0; static void SetupTokenContext( CryptoContext* context); @@ -208,7 +292,7 @@ class QuicSession : public AsyncWrap, ngtcp2_crypto_level crypto_level, uint64_t offset, size_t datalen); - int AckedStreamDataOffset( + void AckedStreamDataOffset( int64_t stream_id, uint64_t offset, size_t datalen); @@ -223,6 +307,7 @@ class QuicSession : public AsyncWrap, int64_t id); void HandshakeCompleted(); inline bool IsInClosingPeriod(); + inline bool IsInDrainingPeriod(); int PathValidation( const ngtcp2_path* path, ngtcp2_path_validation_result res); @@ -423,6 +508,12 @@ class QuicSession : public AsyncWrap, ngtcp2_conn* conn, uint64_t max_streams, void* user_data); + static int OnExtendMaxStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data); static void OnKeylog(const SSL* ssl, const char* line); @@ -509,6 +600,10 @@ class QuicSession : public AsyncWrap, ngtcp2_cid* cid, uint8_t* token, size_t cidlen); + void ExtendMaxStreamData( + int64_t stream_id, + uint64_t max_data); + int WritePackets(); inline QuicStream* CreateStream( int64_t stream_id); @@ -516,6 +611,24 @@ class QuicSession : public AsyncWrap, void SetLocalAddress( const ngtcp2_addr* addr); + typedef ssize_t(*ngtcp2_close_fn)( + ngtcp2_conn* conn, + ngtcp2_path* path, + uint8_t* dest, + size_t destlen, + uint16_t error_code, + ngtcp2_tstamp ts); + + static inline ngtcp2_close_fn SelectCloseFn(QuicErrorFamily family) { + if (family == QUIC_ERROR_APPLICATION) + return ngtcp2_conn_write_application_close; + return ngtcp2_conn_write_connection_close; + } + + inline bool IsHandshakeSuspended() { + return client_hello_cb_running_ || cert_cb_running_; + } + virtual ngtcp2_crypto_level GetServerCryptoLevel() = 0; virtual ngtcp2_crypto_level GetClientCryptoLevel() = 0; virtual void SetServerCryptoLevel(ngtcp2_crypto_level level) = 0; @@ -528,17 +641,16 @@ class QuicSession : public AsyncWrap, ngtcp2_crypto_level rx_crypto_level_; ngtcp2_crypto_level tx_crypto_level_; + QuicError last_error_; bool closing_; bool destroyed_; bool initial_; crypto::SSLPointer ssl_; ngtcp2_conn* connection_; SocketAddress remote_address_; - uint8_t tls_alert_; size_t max_pktlen_; uv_timer_t* idle_timer_; QuicSocket* socket_; - size_t nkey_update_; CryptoContext hs_crypto_ctx_; CryptoContext crypto_ctx_; std::vector tx_secret_; @@ -582,7 +694,128 @@ class QuicSession : public AsyncWrap, size_t max_cid_len_; size_t min_cid_len_; + std::string alpn_; + mem::Allocator allocator_; + bool cert_cb_running_; + bool client_hello_cb_running_; + bool is_tls_callback_; + + struct session_stats { + // The timestamp at which the session was created + uint64_t created_at; + // The timestamp at which the handshake was started + uint64_t handshake_start_at; + // The timestamp at which the most recent handshake + // message was sent + uint64_t handshake_send_at; + // The timestamp at which the most recent handshake + // message was received + uint64_t handshake_continue_at; + // The timestamp at which handshake completed + uint64_t handshake_completed_at; + // The timestamp at which the most recently sent + // non-handshake packets were sent + uint64_t session_sent_at; + // The timestamp at which the most recently received + // non-handshake packets were received + uint64_t session_received_at; + // The timestamp at which a graceful close was started + uint64_t closing_at; + // The total number of bytes received (and not ignored) + // by this QuicSession + uint64_t bytes_received; + // The total number of bytes sent by this QuicSession + uint64_t bytes_sent; + // The total bidirectional stream count + uint64_t bidi_stream_count; + // The total unidirectional stream count + uint64_t uni_stream_count; + // The total number of peer-initiated streams + uint64_t streams_in_count; + // The total number of local-initiated streams + uint64_t streams_out_count; + // The total number of keyupdates + uint64_t keyupdate_count; + }; + session_stats session_stats_{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + AliasedBigUint64Array stats_buffer_; + + template + void IncrementSocketStat( + uint64_t amount, + session_stats* a, + Members... mems) { + IncrementStat(amount, a, mems...); + } + + class TLSHandshakeCallbackScope { + public: + explicit TLSHandshakeCallbackScope(QuicSession* session) : + session_(session) { + session_->is_tls_callback_ = true; + } + + ~TLSHandshakeCallbackScope() { + session_->is_tls_callback_ = false; + } + + static bool IsInTLSHandshakeCallback(QuicSession* session) { + return session->is_tls_callback_; + } + + private: + QuicSession* session_; + }; + + class TLSHandshakeScope { + public: + TLSHandshakeScope(QuicSession* session, bool* monitor) : + session_{session}, + monitor_(monitor) {} + + ~TLSHandshakeScope() { + if (session_->IsHandshakeSuspended()) { + // There are a couple of monitor fields in QuicSession + // (cert_cb_running_ and client_hello_cb_running_). + // When one of those are true, IsHandshakeSuspended + // will be true. We set the monitor to false so we + // can keep the handshake going when the TLS Handshake + // is continued. + *monitor_ = false; + // Only continue the TLS handshake if we are not currently running + // synchronously within the TLS handshake function. This can happen + // when the callback function passed to the clientHello and cert + // event handlers is called synchronously. If the function is called + // asynchronously, then we have to manually continue the handshake. + if (!TLSHandshakeCallbackScope::IsInTLSHandshakeCallback(session_)) { + session_->TLSHandshake(); + session_->SendPendingData(); + } + } + } + + private: + QuicSession* session_; + bool* monitor_; + }; + + // SendScope will cause the session to flush it's + // current pending data queue to the underlying + // socket. + class SendScope { + public: + explicit SendScope(QuicSession* session) : session_(session) {} + ~SendScope() { + if (session_->IsDestroyed()) + return; + session_->SendPendingData(); + session_->StartIdleTimer(-1); + } + private: + QuicSession* session_; + }; friend class QuicServerSession; friend class QuicClientSession; @@ -601,7 +834,10 @@ class QuicServerSession : public QuicSession { const struct sockaddr* addr, const ngtcp2_cid* dcid, const ngtcp2_cid* ocid, - uint32_t version); + uint32_t version, + const std::string& alpn = NGTCP2_ALPN_H3, + bool reject_unauthorized = true, + bool request_cert_ = true); void AddToSocket(QuicSocket* socket) override; @@ -613,9 +849,17 @@ class QuicServerSession : public QuicSession { bool IsDraining(); bool IsServer() const override { return true; } + int OnClientHello() override; + void OnClientHelloDone() override; + int OnTLSStatus() override; bool MaybeTimeout() override; + int OnCert() override; + void OnCertDone( + crypto::SecureContext* context, + v8::Local ocsp_response) override; + const ngtcp2_cid* rcid() const; ngtcp2_cid* pscid(); @@ -631,7 +875,10 @@ class QuicServerSession : public QuicSession { const struct sockaddr* addr, const ngtcp2_cid* dcid, const ngtcp2_cid* ocid, - uint32_t version); + uint32_t version, + const std::string& alpn, + bool reject_unauthorized, + bool request_cert); void DisassociateCID( const ngtcp2_cid* cid) override; @@ -639,8 +886,7 @@ class QuicServerSession : public QuicSession { const ngtcp2_path* path, const uint8_t* data, size_t datalen) override; - int HandleError( - int code) override; + int HandleError() override; void InitTLS_Post() override; void OnIdleTimeout() override; int OnKey( @@ -654,15 +900,14 @@ class QuicServerSession : public QuicSession { const struct sockaddr* addr, unsigned int flags) override; void RemoveFromSocket() override; - int SendConnectionClose( - int error) override; + bool SendConnectionClose() override; int SendPendingData( bool retransmit = false) override; int TLSHandshake_Complete() override; int TLSHandshake_Initial() override; int TLSRead() override; - int StartClosingPeriod(int error); + int StartClosingPeriod(); void StartDrainingPeriod(); ngtcp2_crypto_level GetServerCryptoLevel() override; @@ -674,8 +919,11 @@ class QuicServerSession : public QuicSession { ngtcp2_cid pscid_; ngtcp2_cid rcid_; bool draining_; + bool reject_unauthorized_; + bool request_cert_; MallocedBuffer conn_closebuf_; + v8::Global ocsp_response_; const ngtcp2_conn_callbacks callbacks_ = { nullptr, @@ -707,7 +955,7 @@ class QuicServerSession : public QuicSession { OnStreamReset, OnExtendMaxStreamsBidi, OnExtendMaxStreamsUni, - nullptr // extend_max_stream_data + OnExtendMaxStreamData }; friend class QuicSession; @@ -731,7 +979,9 @@ class QuicClientSession : public QuicSession { v8::Local session_ticket, v8::Local dcid, int select_preferred_address_policy = - QUIC_PREFERRED_ADDRESS_IGNORE); + QUIC_PREFERRED_ADDRESS_IGNORE, + const std::string& alpn = NGTCP2_ALPN_H3, + bool request_ocsp = false); QuicClientSession( QuicSocket* socket, @@ -744,12 +994,16 @@ class QuicClientSession : public QuicSession { v8::Local early_transport_params, v8::Local session_ticket, v8::Local dcid, - int select_preferred_address_policy); + int select_preferred_address_policy, + const std::string& alpn, + bool request_ocsp); void AddToSocket(QuicSocket* socket) override; bool MaybeTimeout() override; + int OnTLSStatus() override; + int SetSocket( QuicSocket* socket, bool nat_rebinding = false); @@ -774,8 +1028,7 @@ class QuicClientSession : public QuicSession { uint64_t max_streams) override; int ExtendMaxStreamsBidi( uint64_t max_streams) override; - int HandleError( - int code) override; + int HandleError() override; void InitTLS_Post() override; void OnIdleTimeout() override; int OnKey( @@ -793,8 +1046,7 @@ class QuicClientSession : public QuicSession { int SelectPreferredAddress( ngtcp2_addr* dest, const ngtcp2_preferred_addr* paddr) override; - int SendConnectionClose( - int error) override; + bool SendConnectionClose() override; int SendPendingData( bool retransmit = false) override; int Start() override; @@ -827,6 +1079,7 @@ class QuicClientSession : public QuicSession { MaybeStackBuffer transportParams_; int select_preferred_address_policy_; + bool request_ocsp_; const ngtcp2_conn_callbacks callbacks_ = { OnClientInitial, @@ -858,7 +1111,7 @@ class QuicClientSession : public QuicSession { nullptr, // stream_reset nullptr, // extend_max_remote_streams_bidi nullptr, // extend_max_remote_streams_uni - nullptr // extend_max_stream_data + OnExtendMaxStreamData }; friend class QuicSession; diff --git a/src/node_quic_socket.cc b/src/node_quic_socket.cc index 4960a61832..dd6810c545 100644 --- a/src/node_quic_socket.cc +++ b/src/node_quic_socket.cc @@ -24,6 +24,7 @@ using v8::Integer; using v8::Isolate; using v8::Local; using v8::Object; +using v8::PropertyAttribute; using v8::String; using v8::Value; @@ -67,20 +68,38 @@ QuicSocket::QuicSocket( validate_addr_(validate_address), max_connections_per_host_(max_connections_per_host), server_secure_context_(nullptr), + server_alpn_(NGTCP2_ALPN_H3), + reject_unauthorized_(false), + request_cert_(false), token_crypto_ctx_{}, - retry_token_expiration_(retry_token_expiration) { + retry_token_expiration_(retry_token_expiration), + stats_buffer_( + env->isolate(), + sizeof(socket_stats_) / sizeof(uint64_t), + reinterpret_cast(&socket_stats_)) { CHECK_EQ(uv_udp_init(env->event_loop(), &handle_), 0); Debug(this, "New QuicSocket created."); QuicSession::SetupTokenContext(&token_crypto_ctx_); EntropySource(token_secret_.data(), token_secret_.size()); + socket_stats_.created_at = uv_hrtime(); + + USE(wrap->DefineOwnProperty( + env->context(), + env->stats_string(), + stats_buffer_.GetJSArray(), + PropertyAttribute::ReadOnly)); } QuicSocket::~QuicSocket() { CHECK(sessions_.empty()); CHECK(dcid_to_scid_.empty()); + uint64_t now = uv_hrtime(); Debug(this, "QuicSocket destroyed.\n" + " Duration: %llu\n" + " Bound Duration: %llu\n" + " Listen Duration: %llu\n" " Bytes Received: %llu\n" " Bytes Sent: %llu\n" " Packets Received: %llu\n" @@ -88,6 +107,9 @@ QuicSocket::~QuicSocket() { " Server Sessions: %llu\n" " Client Sessions: %llu\n" " Retransmit Count: %llu", + now - socket_stats_.created_at, + socket_stats_.bound_at > 0 ? now - socket_stats_.bound_at : 0, + socket_stats_.listen_at > 0 ? now - socket_stats_.listen_at : 0, socket_stats_.bytes_received, socket_stats_.bytes_sent, socket_stats_.packets_received, @@ -151,6 +173,7 @@ int QuicSocket::Bind( #endif arg = Integer::New(env()->isolate(), fd); MakeCallback(env()->quic_on_socket_ready_function(), 1, &arg); + socket_stats_.bound_at = uv_hrtime(); Debug(this, "Bind successful."); return 0; } @@ -166,7 +189,10 @@ SocketAddress* QuicSocket::GetLocalAddress() { void QuicSocket::Listen( SecureContext* sc, - const sockaddr* preferred_address) { + const sockaddr* preferred_address, + const std::string& alpn, + bool reject_unauthorized, + bool request_cert) { // TODO(@jasnell): Should we allow calling listen multiple times? // For now, we guard against it, but we may want to allow it later. CHECK_NOT_NULL(sc); @@ -175,7 +201,11 @@ void QuicSocket::Listen( Debug(this, "Starting to listen."); server_session_config_.Set(env(), preferred_address); server_secure_context_ = sc; + server_alpn_ = alpn; + reject_unauthorized_ = reject_unauthorized; + request_cert_ = request_cert; server_listening_ = true; + socket_stats_.listen_at = uv_hrtime(); ReceiveStart(); } @@ -223,6 +253,7 @@ void QuicSocket::Receive( const struct sockaddr* addr, unsigned int flags) { Debug(this, "Receiving %d bytes from the UDP socket.", nread); + IncrementSocketStat(nread, &socket_stats_, &socket_stats::bytes_received); ngtcp2_pkt_hd hd; int err; @@ -257,47 +288,38 @@ void QuicSocket::Receive( if (scid_it == std::end(dcid_to_scid_)) { Debug(this, "There is no existing session for dcid %s", dcid_hex.c_str()); if (!server_listening_) { - Debug(this, "Ignoring unhandled packet."); + Debug(this, "Ignoring packet because socket is not listening."); return; } - Debug(this, "Dispatching packet to server."); session = ServerReceive(&dcid, &hd, nread, data, addr, flags); if (!session) { Debug(this, "Could not initialize a new QuicServerSession."); - // TODO(@jasnell): What should we do here? + // TODO(@jasnell): Should this be fatal for the QuicSocket? return; } IncrementSocketStat(1, &socket_stats_, &socket_stats::server_sessions); } else { - Debug(this, "An existing QuicSession for this packet was found."); session_it = sessions_.find((*scid_it).second); session = (*session_it).second; CHECK_NE(session_it, std::end(sessions_)); } } else { - Debug(this, "An existing QuicSession for this packet was found."); session = (*session_it).second; } CHECK_NOT_NULL(session); + Debug(this, "Dispatching packet to session"); // An appropriate handler was found! Dispatch the data if (session->IsDestroyed()) { - // Ignoring packet for destroyed session + Debug(this, "Ignoring packet because session is destroyed"); return; } - Debug(this, "Dispatching packet to session for dcid %s", dcid_hex.c_str()); err = session->Receive(&hd, nread, data, addr, flags); if (err != 0) { - Debug(this, - "The QuicSession failed to process the packet successfully. Error %d", - err); + Debug(this, "Ignoring unsuccessfully processed packet. Error %d", err); return; } - - IncrementSocketStat(nread, &socket_stats_, &socket_stats::bytes_received); IncrementSocketStat(1, &socket_stats_, &socket_stats::packets_received); - - SendPendingData(); } int QuicSocket::ReceiveStart() { @@ -328,22 +350,6 @@ void QuicSocket::ReportSendError(int error) { return; } -void QuicSocket::SendPendingData( - bool retransmit) { - - HandleScope handle_scope(env()->isolate()); - InternalCallbackScope callback_scope(this); - - Debug(this, "Sending pending data. Retransmit? %s", - retransmit ? "yes" : "no"); - for (auto session : sessions_) { - int err = session.second->SendPendingData(retransmit); - if (err != 0) { - // TODO(@jasnell): handle error - } - } -} - int QuicSocket::SendVersionNegotiation( const ngtcp2_pkt_hd* chd, const sockaddr* addr) { @@ -479,8 +485,10 @@ std::shared_ptr QuicSocket::ServerReceive( addr, &hd->scid, pocid, - hd->version); - + hd->version, + server_alpn_, + reject_unauthorized_, + request_cert_); Local arg = session->object(); MakeCallback(env()->quic_on_session_ready_function(), 1, &arg); @@ -787,7 +795,19 @@ void QuicSocketListen(const FunctionCallbackInfo& args) { } } - socket->Listen(sc, preferred_address); + std::string alpn(NGTCP2_ALPN_H3); + if (args[4]->IsString()) { + Utf8Value val(env->isolate(), args[4]); + alpn = val.length(); + alpn += *val; + } + + socket->Listen( + sc, + preferred_address, + alpn, + args[5]->IsTrue(), // reject_unauthorized + args[6]->IsTrue()); // request_cert } void QuicSocketReceiveStart(const FunctionCallbackInfo& args) { diff --git a/src/node_quic_socket.h b/src/node_quic_socket.h index 4b9224d40c..aaf5b661c3 100644 --- a/src/node_quic_socket.h +++ b/src/node_quic_socket.h @@ -65,7 +65,10 @@ class QuicSocket : public HandleWrap { SocketAddress* GetLocalAddress(); void Listen( crypto::SecureContext* context, - const sockaddr* preferred_address = nullptr); + const sockaddr* preferred_address = nullptr, + const std::string& alpn = NGTCP2_ALPN_H3, + bool reject_unauthorized = true, + bool request_cert = false); int ReceiveStart(); int ReceiveStop(); void RemoveSession( @@ -73,8 +76,6 @@ class QuicSocket : public HandleWrap { const sockaddr* addr); void ReportSendError( int error); - void SendPendingData( - bool retransmit = false); int SetBroadcast( bool on); int SetMulticastInterface( @@ -160,6 +161,9 @@ class QuicSocket : public HandleWrap { size_t max_connections_per_host_; QuicSessionConfig server_session_config_; crypto::SecureContext* server_secure_context_; + std::string server_alpn_; + bool reject_unauthorized_; + bool request_cert_; std::unordered_map> sessions_; std::unordered_map dcid_to_scid_; CryptoContext token_crypto_ctx_; @@ -178,6 +182,12 @@ class QuicSocket : public HandleWrap { addr_counts_; struct socket_stats { + // The timestamp at which the socket was created + uint64_t created_at; + // The timestamp at which the socket was bound + uint64_t bound_at; + // The timestamp at which the socket began listening + uint64_t listen_at; // The total number of bytes received (and not ignored) // by this QuicSocket instance. uint64_t bytes_received; @@ -206,7 +216,9 @@ class QuicSocket : public HandleWrap { // retransmitted by this QuicSocket instance. uint64_t retransmit_count; }; - socket_stats socket_stats_{0, 0, 0, 0, 0, 0, 0}; + socket_stats socket_stats_{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + AliasedBigUint64Array stats_buffer_; template void IncrementSocketStat( diff --git a/src/node_quic_stream.cc b/src/node_quic_stream.cc index f45ea27247..6c92433be3 100644 --- a/src/node_quic_stream.cc +++ b/src/node_quic_stream.cc @@ -11,6 +11,8 @@ #include "node_quic_util.h" #include "v8.h" +#include + namespace node { using v8::Context; @@ -57,66 +59,126 @@ QuicStream::QuicStream( AsyncWrap(session->env(), wrap, AsyncWrap::PROVIDER_QUICSTREAM), StreamBase(session->env()), session_(session), - flags_(0), stream_id_(stream_id), - reset_(false), - should_send_fin_(false), - available_outbound_length_(0) { + flags_(QUICSTREAM_FLAG_INITIAL), + available_outbound_length_(0), + inbound_consumed_data_while_paused_(0), + stats_buffer_( + session->env()->isolate(), + sizeof(stream_stats_) / sizeof(uint64_t), + reinterpret_cast(&stream_stats_)) { CHECK_NOT_NULL(session); + SetInitialFlags(); + session->AddStream(this); StreamBase::AttachToObject(GetObject()); PushStreamListener(&stream_listener_); - session->AddStream(this); + stream_stats_.created_at = uv_hrtime(); + + USE(wrap->DefineOwnProperty( + env()->context(), + env()->stats_string(), + stats_buffer_.GetJSArray(), + PropertyAttribute::ReadOnly)); } QuicStream::~QuicStream() { // Check that Destroy() has been called CHECK_NULL(session_); CHECK_EQ(0, streambuf_.Length()); + uint64_t now = uv_hrtime(); + Debug(this, + "QuicStream %llu destroyed.\n" + " Duration: %llu\n" + " Bytes Received: %llu\n" + " Bytes Sent: %llu\n", + GetID(), + now - stream_stats_.created_at, + stream_stats_.bytes_received, + stream_stats_.bytes_sent); } -void QuicStream::Close( - uint16_t app_error_code) { +inline void QuicStream::SetInitialFlags() { + if (GetDirection() == QUIC_STREAM_UNIDIRECTIONAL) { + if (session_->IsServer()) { + switch (GetOrigin()) { + case QUIC_STREAM_SERVER: + SetReadClose(); + break; + case QUIC_STREAM_CLIENT: + SetWriteClose(); + break; + default: + UNREACHABLE(); + } + } else { + switch (GetOrigin()) { + case QUIC_STREAM_SERVER: + SetWriteClose(); + break; + case QUIC_STREAM_CLIENT: + SetReadClose(); + break; + default: + UNREACHABLE(); + } + } + } +} + +// QuicStream::Close() is called by the QuicSession when ngtcp2 detects that +// a stream has been closed. This, in turn, calls out to the JavaScript to +// start the process of tearing down and destroying the QuicStream instance. +void QuicStream::Close(uint16_t app_error_code) { Debug(this, "Stream %llu closed with code %d", GetID(), app_error_code); + SetReadClose(); + SetWriteClose(); HandleScope scope(env()->isolate()); - flags_ |= QUIC_STREAM_FLAG_CLOSED; + Context::Scope context_context(env()->context()); Local arg = Number::New(env()->isolate(), app_error_code); MakeCallback(env()->quic_on_stream_close_function(), 1, &arg); } +// Receiving a reset means that any data we've accumulated to send +// can be discarded and we don't want to keep writing data, so +// we want to clear our outbound buffers here and notify +// the JavaScript side that we've been reset so that we stop +// pumping data out. void QuicStream::Reset(uint64_t final_size, uint16_t app_error_code) { - // Receiving a reset means that any data we've accumulated to send - // can be discarded and we don't want to keep writing data, so - // we likely want to clear our outbound buffers here and notify - // the JavaScript side that we've been reset so that we stop - // pumping data out. Debug(this, "Resetting stream %llu with app error code %d, and final size %llu", GetID(), app_error_code, final_size); - reset_ = true; HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); streambuf_.Cancel(); Local argv[] = { - Number::New(env()->isolate(), final_size), + Number::New(env()->isolate(), static_cast(final_size)), Integer::New(env()->isolate(), app_error_code) }; MakeCallback(env()->quic_on_stream_reset_function(), arraysize(argv), argv); } void QuicStream::Destroy() { + SetReadClose(); + SetWriteClose(); streambuf_.Cancel(); session_->RemoveStream(stream_id_); session_ = nullptr; } +// Do shutdown is called when the JS stream writable side is closed. +// We want to mark the writable side closed and send pending data. int QuicStream::DoShutdown(ShutdownWrap* req_wrap) { if (IsDestroyed()) return UV_EPIPE; - Debug(this, "Writable side shutdown"); - flags_ |= QUIC_STREAM_FLAG_SHUT; - should_send_fin_ = true; - + // Do nothing if the stream was already shutdown. Specifically, + // we should not attempt to send anything on the QuicSession + if (!IsWritable()) + return 1; + stream_stats_.closing_at = uv_hrtime(); + SetWriteClose(); + session_->SendStreamData(this); return 1; } @@ -125,68 +187,81 @@ int QuicStream::DoWrite( uv_buf_t* bufs, size_t nbufs, uv_stream_t* send_handle) { - CHECK_NULL(send_handle); - // If the stream has been reset, then the writable side of - // the duplex should have been closed and we shouldn't be - // receiving any more data.. but just in case... - if (reset_ || IsDestroyed()) { + // A write should not have happened if we've been destroyed or + // the QuicStream is no longer writable. + if (IsDestroyed() || !IsWritable()) { req_wrap->Done(UV_EOF); return 0; } - // Buffers written must be held on to until acked. The callback - // passed in here will be called when the ack is received. - // TODO(@jasnell): For now, the data will be held onto for - // pretty much eternity, and the implementation will retry an - // unlimited number of times. We need to constrain that to - // fail reasonably after a given number of attempts. - // Specifically, we need to ensure that all of the data is - // cleaned up when the stream is destroyed, even if it hasn't - // been acknowledged. + // There's a difficult balance required here: + // + // Unlike typical UDP, which is fire-and-forget, QUIC packets + // have to be acknowledged. If a packet is not acknowledged + // soon enough, it is retransmitted. The exact arrangement + // of packets being retransmitted varies over the course of + // the connection on many factors, so we can't simply encode + // the packets and resend them. Instead, we have to retain the + // original data and re-encode packets on each transmission + // attempt. This means we have to persist the data written + // until either an acknowledgement is received or the stream + // is reset and canceled. + // + // That said, on the JS Streams API side, we can only write + // one batch of buffers at a time. That is, DoWrite won't be + // called again until the previous DoWrite is completed by + // calling WriteWrap::Done(). The challenge, however, is that + // calling Done() essentially signals that we're done with + // the buffers being written, allowing those to be freed. + // + // In other words, if we just store the given buffers and + // wait to call Done() when we receive an acknowledgement, + // we severely limit our throughput and kill performance + // because the JavaScript side won't be able to send additional + // buffers until we receive the acknowledgement from the peer. + // However, if we call Done() here to allow the next chunk to + // be written, we have to copy the data because the buffers + // may end up being freed once the callback is invoked. The + // memcpy obviously incurs a cost but it'll at least be less + // than waiting for the acknowledgement, allowing data to be + // written faster but at the cost of a data copy. + // + // Because of the need to copy, performing many small writes + // will incur a performance penalty over a smaller number of + // larger writes, but only up to a point. Frequently copying + // large chunks of data will end up slowing things down also. // - // When more than one buffer is passed in, the callback will - // only be invoked when the final buffer in the set is consumed. - uint64_t len = - streambuf_.Push( - bufs, - nbufs, - [&](int status, void* user_data) { - DecrementAvailableOutboundLength(len); - WriteWrap* wrap = static_cast(user_data); - CHECK_NOT_NULL(wrap); - wrap->Done(status); - }, req_wrap, req_wrap->object()); - - IncrementAvailableOutboundLength(len); + // Because we are copying to allow the JS side to write + // faster independently of the underlying send, we will have + // to be careful not to allow the internal buffer to grow + // too large, or we'll run into several other problems. + + uint64_t len = streambuf_.Copy(bufs, nbufs); + IncrementStat(len, &stream_stats_, &stream_stats::bytes_sent); + req_wrap->Done(0); + stream_stats_.stream_sent_at = uv_hrtime(); + session_->SendStreamData(this); + + // IncrementAvailableOutboundLength(len); return 0; } -uint64_t QuicStream::GetID() const { - return stream_id_; -} - -QuicSession* QuicStream::Session() { - return session_; -} - -int QuicStream::AckedDataOffset( - uint64_t offset, - size_t datalen) { +void QuicStream::AckedDataOffset(uint64_t offset, size_t datalen) { + if (IsDestroyed()) + return; streambuf_.Consume(datalen); - return 0; + stream_stats_.stream_acked_at = uv_hrtime(); } -int QuicStream::Send0RTTData() { - return session_->Send0RTTStreamData(this, should_send_fin_, &streambuf_); +size_t QuicStream::DrainInto( + std::vector* vec, + QuicBuffer::drain_from from) { + return streambuf_.DrainInto(vec, from); } -int QuicStream::SendPendingData(bool retransmit) { - return session_->SendStreamData( - this, - should_send_fin_, - &streambuf_, - retransmit ? QuicBuffer::DRAIN_FROM_ROOT : QuicBuffer::DRAIN_FROM_HEAD); +void QuicStream::Commit(size_t count) { + streambuf_.SeekHead(count); } inline void QuicStream::IncrementAvailableOutboundLength(size_t amount) { @@ -197,53 +272,47 @@ inline void QuicStream::DecrementAvailableOutboundLength(size_t amount) { available_outbound_length_ -= amount; } -QuicStream* QuicStream::New( - QuicSession* session, - uint64_t stream_id) { - Local obj; - if (!session->env() - ->quicserverstream_constructor_template() - ->NewInstance(session->env()->context()).ToLocal(&obj)) { - return nullptr; - } - return new QuicStream(session, obj, stream_id); -} - int QuicStream::ReadStart() { CHECK(!this->IsDestroyed()); - Debug(this, "Reading started."); - flags_ |= QUIC_STREAM_FLAG_READ_START; - flags_ &= ~QUIC_STREAM_FLAG_READ_PAUSED; - - // Flush data to JS here? - + CHECK(IsReadable()); + SetReadStart(); + SetReadResume(); + session_->ExtendStreamOffset(this, inbound_consumed_data_while_paused_); return 0; } int QuicStream::ReadStop() { CHECK(!this->IsDestroyed()); - if (!IsReading()) - return 0; - Debug(this, "Reading stopped"); - flags_ |= QUIC_STREAM_FLAG_READ_PAUSED; + CHECK(IsReadable()); + SetReadPause(); return 0; } -int QuicStream::ReceiveData( - int fin, - const uint8_t* data, - size_t datalen) { - Debug(this, "Receiving %d bytes of data", datalen); - if (reset_) { - Debug(this, "Stream has been reset, discarding received data."); - return 0; - } - HandleScope scope(env()->isolate()); - do { +// Passes chunks of data on to the JavaScript side as soon as they are +// received but only if we're still readable. The caller of this must have a +// HandleScope. +// +// Note that this is pushing data to the JS side regardless of whether +// anything is listening. For flow-control, we only send window updates +// to the sending peer if the stream is in flowing mode, so the sender +// should not be sending too much data. +// TODO(@jasnell): We may need to be more defensive here with regards to +// flow control to keep the buffer from growing too much. ngtcp2 may give +// us some protection but we need to verify. +void QuicStream::ReceiveData(int fin, const uint8_t* data, size_t datalen) { + Debug(this, "Receiving %d bytes of data. Final? %s. Readable? %s", + datalen, fin ? "yes" : "no", IsReadable() ? "yes" : "no"); + + if (!IsReadable()) + return; + + IncrementStat(datalen, &stream_stats_, &stream_stats::bytes_received); + + stream_stats_.stream_received_at = uv_hrtime(); + + while (datalen > 0) { uv_buf_t buf = EmitAlloc(datalen); - ssize_t avail = datalen; - if (static_cast(buf.len) < avail) - avail = buf.len; + size_t avail = std::min(static_cast(buf.len), datalen); // TODO(@jasnell): For now, we're allocating and copying. Once // we determine if we can safely switch to a non-allocated mode @@ -256,17 +325,31 @@ int QuicStream::ReceiveData( memcpy(buf.base, data, avail); data += avail; datalen -= avail; - Debug(this, "Emitting %d bytes of data", avail); EmitRead(avail, buf); - } while (datalen != 0); + if (IsReadPaused()) + inbound_consumed_data_while_paused_ += avail; + else + session_->ExtendStreamOffset(this, avail); + } + // When fin != 0, we've received that last chunk of data for this + // stream, indicating that the stream is no longer readable. if (fin) { - Debug(this, "Emitting EOF"); + SetReadClose(); EmitRead(UV_EOF); - Session()->ShutdownStreamRead(stream_id_); } +} - return 0; +QuicStream* QuicStream::New( + QuicSession* session, + uint64_t stream_id) { + Local obj; + if (!session->env() + ->quicserverstream_constructor_template() + ->NewInstance(session->env()->context()).ToLocal(&obj)) { + return nullptr; + } + return new QuicStream(session, obj, stream_id); } // JavaScript API diff --git a/src/node_quic_stream.h b/src/node_quic_stream.h index 4b195d3b72..0bf8d2b27d 100644 --- a/src/node_quic_stream.h +++ b/src/node_quic_stream.h @@ -17,58 +17,138 @@ namespace quic { class QuicSession; class QuicServerSession; -enum quic_stream_flags { - QUIC_STREAM_FLAG_NONE = 0x0, - // Writable side has ended - QUIC_STREAM_FLAG_SHUT = 0x1, - // Reading has started - QUIC_STREAM_FLAG_READ_START = 0x2, - // Reading is paused - QUIC_STREAM_FLAG_READ_PAUSED = 0x4, - // Stream is closed - QUIC_STREAM_FLAG_CLOSED = 0x8, - // Stream has received all the data it can - QUIC_STREAM_FLAG_EOS = 0x20 -}; - class QuicStreamListener : public StreamListener { public: uv_buf_t OnStreamAlloc(size_t suggested_size) override; void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override; }; +// QuicStream's are simple data flows that, fortunately, do not +// require much. They may be: +// +// * Bidirectional or Unidirectional +// * Server or Client Initiated +// +// The flow direction and origin of the stream are important in +// determining the write and read state (Open or Closed). Specifically: +// +// A Unidirectional stream originating with the Server is: +// +// * Server Writable (Open) but not Client Writable (Closed) +// * Client Readable (Open) but not Server Readable (Closed) +// +// Likewise, a Unidirectional stream originating with the +// Client is: +// +// * Client Writable (Open) but not Server Writable (Closed) +// * Server Readable (Open) but not Client Readable (Closed) +// +// Bidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// +// Unidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// +// The Closed states is terminal. A stream may be destroyed +// naturally when both the read and write states are Closed. +// Although, any stream may be abruptly terminated at any time. +// +// A stream that is Open Writable may have data pending or not. +// +// A QuicSession should only attempt to send stream data when (a) there +// is data pending to send of (b) there is no remaining data to send and +// the writable side is ready to transition to Closed. class QuicStream : public AsyncWrap, public StreamBase { public: + enum QuicStreamDirection { + QUIC_STREAM_BIRECTIONAL, + QUIC_STREAM_UNIDIRECTIONAL + }; + + enum QuicStreamOrigin { + QUIC_STREAM_SERVER, + QUIC_STREAM_CLIENT + }; + static void Initialize( Environment* env, v8::Local target, v8::Local context); - static QuicStream* New( - QuicSession* session, - uint64_t stream_id); - - QuicStream( - QuicSession* session, - v8::Local target, - uint64_t stream_id); + static QuicStream* New(QuicSession* session, uint64_t stream_id); ~QuicStream() override; - uint64_t GetID() const; - QuicSession* Session(); + inline QuicStreamDirection GetDirection() const { + return stream_id_ & 0b10 ? + QUIC_STREAM_UNIDIRECTIONAL : + QUIC_STREAM_BIRECTIONAL; + } - virtual int AckedDataOffset( - uint64_t offset, - size_t datalen); + inline QuicStreamOrigin GetOrigin() const { + return stream_id_ & 0b01 ? + QUIC_STREAM_SERVER : + QUIC_STREAM_CLIENT; + } + + uint64_t GetID() const { return stream_id_; } - virtual void Close( - uint16_t app_error_code); + inline bool IsDestroyed() { + return session_ == nullptr; + } + + inline bool IsWritable() { + return (flags_ & QUICSTREAM_FLAG_WRITE) == 0; + } + + inline bool IsReadable() { + return (flags_ & QUICSTREAM_FLAG_READ) == 0; + } + + inline bool IsReadStarted() { + return flags_ & QUICSTREAM_FLAG_READ_STARTED; + } + + inline bool IsReadPaused() { + return flags_ & QUICSTREAM_FLAG_READ_PAUSED; + } + + bool IsAlive() override { + return !IsDestroyed() && !IsClosing(); + } + + bool IsClosing() override { + return !IsWritable() && !IsReadable(); + } - virtual void Reset( - uint64_t final_size, - uint16_t app_error_code); + QuicSession* Session() { return session_; } + + virtual void AckedDataOffset(uint64_t offset, size_t datalen); + + virtual void Close(uint16_t app_error_code = 0); + + virtual void Reset(uint64_t final_size, uint16_t app_error_code = 0); virtual void Destroy(); @@ -78,28 +158,10 @@ class QuicStream : public AsyncWrap, size_t nbufs, uv_stream_t* send_handle) override; - inline void IncrementAvailableOutboundLength( - size_t amount); - inline void DecrementAvailableOutboundLength( - size_t amount); - - bool IsAlive() override { - return !IsDestroyed() && !IsShutdown() && !IsClosing(); - } - bool IsClosing() override { - return flags_ & QUIC_STREAM_FLAG_SHUT || - flags_ & QUIC_STREAM_FLAG_EOS; - } - bool IsDestroyed() { return session_ == nullptr; } - bool IsEnded() { return flags_ & QUIC_STREAM_FLAG_EOS; } - bool IsPaused() { return flags_ & QUIC_STREAM_FLAG_READ_PAUSED; } - bool IsReading() { return flags_ & QUIC_STREAM_FLAG_READ_START; } - bool IsShutdown() { return flags_ & QUIC_STREAM_FLAG_SHUT; } + inline void IncrementAvailableOutboundLength(size_t amount); + inline void DecrementAvailableOutboundLength(size_t amount); - virtual int ReceiveData( - int fin, - const uint8_t* data, - size_t datalen); + virtual void ReceiveData(int fin, const uint8_t* data, size_t datalen); // Required for StreamBase int ReadStart() override; @@ -108,12 +170,13 @@ class QuicStream : public AsyncWrap, int ReadStop() override; // Required for StreamBase - int DoShutdown( - ShutdownWrap* req_wrap) override; + int DoShutdown(ShutdownWrap* req_wrap) override; + + size_t DrainInto( + std::vector* vec, + QuicBuffer::drain_from from); - int Send0RTTData(); - int SendPendingData( - bool retransmit = false); + void Commit(size_t count); AsyncWrap* GetAsyncWrap() override { return this; } @@ -129,15 +192,70 @@ class QuicStream : public AsyncWrap, SET_SELF_SIZE(QuicStream) private: + QuicStream( + QuicSession* session, + v8::Local target, + uint64_t stream_id); + + inline void SetInitialFlags(); + + enum Flags { + QUICSTREAM_FLAG_INITIAL = 0, + QUICSTREAM_FLAG_READ = 1, + QUICSTREAM_FLAG_WRITE = 2, + QUICSTREAM_FLAG_READ_STARTED = 3, + QUICSTREAM_FLAG_READ_PAUSED = 8 + }; + + inline void SetWriteClose() { + flags_ |= QUICSTREAM_FLAG_WRITE; + } + + inline void SetReadClose() { + flags_ |= QUICSTREAM_FLAG_READ; + } + + inline void SetReadStart() { + flags_ |= QUICSTREAM_FLAG_READ_STARTED; + } + + inline void SetReadPause() { + flags_ |= QUICSTREAM_FLAG_READ_PAUSED; + } + + inline void SetReadResume() { + flags_ &= QUICSTREAM_FLAG_READ_PAUSED; + } + QuicStreamListener stream_listener_; QuicSession* session_; - uint32_t flags_; uint64_t stream_id_; - bool reset_; + uint32_t flags_; QuicBuffer streambuf_; - bool should_send_fin_; size_t available_outbound_length_; + size_t inbound_consumed_data_while_paused_; + + struct stream_stats { + // The timestamp at which the stream was created + uint64_t created_at; + // The timestamp at which the stream most recently sent data + uint64_t stream_sent_at; + // The timestamp at which the stream most recently received data + uint64_t stream_received_at; + // The timestamp at which the stream most recently received an + // acknowledgement for data + uint64_t stream_acked_at; + // The timestamp at which a graceful close started + uint64_t closing_at; + // The total number of bytes received + uint64_t bytes_received; + // The total number of bytes sent + uint64_t bytes_sent; + }; + stream_stats stream_stats_{0, 0, 0, 0, 0, 0, 0}; + + AliasedBigUint64Array stats_buffer_; }; } // namespace quic diff --git a/src/node_quic_util.h b/src/node_quic_util.h index c8cd563ce9..384107952b 100644 --- a/src/node_quic_util.h +++ b/src/node_quic_util.h @@ -32,11 +32,17 @@ constexpr uint64_t DEFAULT_RETRYTOKEN_EXPIRATION = 10ULL; #define RETURN_IF_FAIL(test, success, ret) \ do { \ - if ((test) != (success)) return (ret); \ + if (UNLIKELY((test) != (success))) return (ret); \ } while (0) #define RETURN_IF_FAIL_OPENSSL(test) RETURN_IF_FAIL(test, 1, -1) +#define RETURN_RET_IF_FAIL(test, success) \ + do { \ + int ret = test; \ + if (UNLIKELY((ret) != (success))) return (ret); \ + } while (0) + enum SelectPreferredAddressPolicy { // Ignore the server-provided preferred address QUIC_PREFERRED_ADDRESS_IGNORE, @@ -55,6 +61,50 @@ inline void hash_combine(size_t* seed, const T& value, Args... rest) { hash_combine(seed, rest...); } +// QUIC error codes generally fall into two distinct namespaces: +// Connection Errors and Application Errors. Connection errors +// are further subdivided into Crypto and non-Crypto. Application +// errors are entirely specific to the QUIC application being +// used. An easy rule of thumb is that Application errors are +// semantically associated with the ALPN identifier negotiated +// for the QuicSession. So, if a connection is closed with +// family: QUIC_ERROR_APPLICATION and code: 123, you have to +// look at the ALPN identifier to determine exactly what it +// means. Connection (Session) and Crypto errors, on the other +// hand, share the same meaning regardless of the ALPN. +enum QuicErrorFamily { + QUIC_ERROR_SESSION, + QUIC_ERROR_CRYPTO, + QUIC_ERROR_APPLICATION +}; + +struct QuicError { + QuicErrorFamily family; + int code; + inline QuicError( + QuicErrorFamily family_ = QUIC_ERROR_SESSION, + int code_ = NGTCP2_NO_ERROR) : + family(family_), code(code_) {} +}; + +inline QuicError InitQuicError( + QuicErrorFamily family = QUIC_ERROR_SESSION, + int code_ = NGTCP2_NO_ERROR) { + QuicError error; + error.family = family; + switch (family) { + case QUIC_ERROR_CRYPTO: + code_ |= NGTCP2_CRYPTO_ERROR; + // Fall-through... + case QUIC_ERROR_SESSION: + error.code = ngtcp2_err_infer_quic_transport_error_code(code_); + break; + case QUIC_ERROR_APPLICATION: + error.code = code_; + } + return error; +} + class SocketAddress { public: // std::hash specialization for sockaddr instances (ipv4 or ipv6) used @@ -298,6 +348,18 @@ decltype(auto) access(C* cls, T C::*member, Mems... rest) { return access((cls->*member), rest...); } +template +void IncrementStat( + uint64_t amount, + A* a, + Members... mems) { + static uint64_t max = std::numeric_limits::max(); + uint64_t current = access(a, mems...); + uint64_t delta = std::min(amount, max - current); + access(a, mems...) += delta; +} + + typedef int(*install_fn)( ngtcp2_conn* conn, const uint8_t* key, diff --git a/test/parallel/test-quic-client-server.js b/test/parallel/test-quic-client-server.js index 225c5c4ceb..848040d8e9 100644 --- a/test/parallel/test-quic-client-server.js +++ b/test/parallel/test-quic-client-server.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; // Tests a simple QUIC client/server round-trip @@ -6,16 +7,26 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { internalBinding } = require('internal/test/binding'); +const { + constants: { + NGTCP2_NO_ERROR, + QUIC_ERROR_APPLICATION, + } +} = internalBinding('quic'); + const { Buffer } = require('buffer'); const Countdown = require('../common/countdown'); const assert = require('assert'); const fs = require('fs'); const fixtures = require('../common/fixtures'); -const key = fixtures.readKey('agent8-key.pem', 'binary'); -const cert = fixtures.readKey('agent8-cert.pem', 'binary'); +const key = fixtures.readKey('agent1-key.pem', 'binary'); +const cert = fixtures.readKey('agent1-cert.pem', 'binary'); +const ca = fixtures.readKey('fake-startcom-root-cert.pem', 'binary'); const { debuglog } = require('util'); const debug = debuglog('test'); +const filedata = fs.readFileSync(__filename, { encoding: 'utf8' }); const { createSocket } = require('quic'); @@ -24,81 +35,174 @@ const server = createSocket({ type: 'udp4', port: 0 }); const unidata = ['I wonder if it worked.', 'test']; const kServerName = 'test'; -const kALPN = 'h3-20'; +const kALPN = 'zzz'; // ALPN can be overriden to whatever we want + const kKeylogs = [ + /QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET.*/, /SERVER_HANDSHAKE_TRAFFIC_SECRET.*/, + /QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET.*/, /CLIENT_HANDSHAKE_TRAFFIC_SECRET.*/, + /QUIC_SERVER_TRAFFIC_SECRET_0.*/, /EXPORTER_SECRET.*/, /SERVER_TRAFFIC_SECRET_0.*/, - /CLIENT_TRAFFIC_SECRET_0.*/ + /QUIC_CLIENT_TRAFFIC_SECRET_0.*/, + /CLIENT_TRAFFIC_SECRET_0.*/, ]; + const countdown = new Countdown(2, () => { debug('Countdown expired. Destroying sockets'); server.close(); client.close(); }); -server.listen({ key, cert }); +server.listen({ + key, + cert, + ca, + requestCert: true, + rejectUnauthorized: false, + alpn: kALPN +}); server.on('session', common.mustCall((session) => { debug('QuicServerSession Created'); + session.on('clientHello', common.mustCall( + (alpn, servername, ciphers, cb) => { + assert.strictEqual(alpn, kALPN); + assert.strictEqual(servername, kServerName); + assert.strictEqual(ciphers.length, 4); + cb(); + })); + + session.on('OCSPRequest', common.mustCall( + (servername, context, cb) => { + debug('QuicServerSession received a OCSP request'); + assert.strictEqual(servername, kServerName); + + // This will be a SecureContext. By default it will + // be the SecureContext used to create the QuicSession. + // If the user wishes to do something with it, it can, + // but if it wishes to pass in a new SecureContext, + // it can pass it in as the second argument to the + // callback below. + assert(context); + debug('QuicServerSession Certificate: ', context.getCertificate()); + debug('QuicServerSession Issuer: ', context.getIssuer()); + + // The callback can be invoked asynchronously + // TODO(@jasnell): Using setImmediate here causes the test + // to fail, but it shouldn't. Investigate why. + process.nextTick(() => { + // The first argument is a potential error, + // in which case the session will be destroyed + // immediately. + // The second is an optional new SecureContext + // The third is the ocsp response. + // All arguments are optional + cb(null, null, Buffer.from('hello')); + }); + })); + session.on('keylog', common.mustCall((line) => { assert(kKeylogs.shift().test(line)); - }), kKeylogs.length); + }, kKeylogs.length)); - session.on('secure', common.mustCall((servername, alpn) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { debug('QuicServerSession TLS Handshake Complete'); - debug('Server name: %s', servername); - debug('ALPN: %s', alpn); + debug(' Server name: %s', servername); + debug(' ALPN: %s', alpn); + debug(' Cipher: %s, %s', cipher.name, cipher.version); assert.strictEqual(session.servername, servername); assert.strictEqual(servername, kServerName); assert.strictEqual(session.alpnProtocol, alpn); + assert.strictEqual(session.getPeerCertificate().subject.CN, 'agent1'); const uni = session.openStream({ halfOpen: true }); uni.write(unidata[0]); uni.end(unidata[1]); debug('Unidirectional, Server-initiated stream %d opened', uni.id); + uni.on('data', common.mustNotCall()); + uni.on('finish', common.mustCall()); + uni.on('close', common.mustCall()); + uni.on('end', common.mustCall()); })); session.on('stream', common.mustCall((stream) => { debug('Bidirectional, Client-initiated stream %d received', stream.id); const file = fs.createReadStream(__filename); + let data = ''; file.pipe(stream); stream.setEncoding('utf8'); - stream.resume(); - stream.on('end', common.mustCall()); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Server received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + })); + + session.on('close', common.mustCall(() => { + const { + code, + family + } = session.closeCode; + debug(`Server sesion closed with code ${code} (family: ${family})`); + assert.strictEqual(code, NGTCP2_NO_ERROR); + assert.strictEqual(family, QUIC_ERROR_APPLICATION); })); })); server.on('ready', common.mustCall(() => { debug('Server is listening on port %d', server.address.port); - client = createSocket({ type: 'udp6', port: 0 }); + client = createSocket({ + type: 'udp4', + port: 0, + client: { + type: 'udp4', + key, + cert, + ca, + maxStreamsUni: 1000, + minCidLen: 5, + maxCidLen: 10, + alpn: kALPN, + } + }); + const req = client.connect({ - type: 'udp6', address: 'localhost', port: server.address.port, - rejectUnauthorized: false, - maxStreamsUni: 1000, servername: kServerName, - minCidLen: 5, - maxCidLen: 10, + requestOCSP: true, }); + client.on('close', () => debug('Client closing')); + assert.strictEqual(req.servername, kServerName); + req.on('OCSPResponse', common.mustCall((response) => { + debug(`QuicClientSession OCSP response: "${response.toString()}"`); + assert.strictEqual(response.toString(), 'hello'); + })); + req.on('sessionTicket', common.mustCall((id, ticket, params) => { debug('Session ticket received'); assert(id instanceof Buffer); assert(ticket instanceof Buffer); assert(params instanceof Buffer); + debug(' ID: %s', id.toString('hex')); + debug(' Ticket: %s', ticket.toString('hex')); + debug(' Params: %s', params.toString('hex')); }, 2)); - req.on('secure', common.mustCall((servername, alpn) => { + req.on('secure', common.mustCall((servername, alpn, cipher) => { debug('QuicClientSession TLS Handshake Complete'); - debug('Server name: %s', servername); - debug('ALPN: %s', alpn); + debug(' Server name: %s', servername); + debug(' ALPN: %s', alpn); + debug(' Cipher: %s, %s', cipher.name, cipher.version); assert.strictEqual(servername, kServerName); assert.strictEqual(req.servername, kServerName); assert.strictEqual(alpn, kALPN); @@ -109,7 +213,15 @@ server.on('ready', common.mustCall(() => { const file = fs.createReadStream(__filename); const stream = req.openStream(); file.pipe(stream); + let data = ''; stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('finish', common.mustCall()); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Client received expected data for stream %d', stream.id); + })); stream.on('close', common.mustCall(() => { debug('Bidirectional, Client-initiated stream %d closed', stream.id); countdown.dec(); @@ -124,6 +236,7 @@ server.on('ready', common.mustCall(() => { stream.on('data', (chunk) => data += chunk); stream.on('end', common.mustCall(() => { assert.strictEqual(data, unidata.join('')); + debug('Client received expected data for stream %d', stream.id); })); stream.on('close', common.mustCall(() => { debug('Unidirectional, Server-initiated stream %d closed', stream.id); @@ -131,6 +244,16 @@ server.on('ready', common.mustCall(() => { })); })); + req.on('close', common.mustCall(() => { + const { + code, + family + } = req.closeCode; + debug(`Client sesion closed with code ${code} (family: ${family})`); + assert.strictEqual(code, NGTCP2_NO_ERROR); + assert.strictEqual(family, QUIC_ERROR_APPLICATION); + })); })); server.on('listening', common.mustCall()); +server.on('close', () => debug('Server closing'));