diff options
Diffstat (limited to 'src/quic_conn.c')
-rw-r--r-- | src/quic_conn.c | 1893 |
1 files changed, 1893 insertions, 0 deletions
diff --git a/src/quic_conn.c b/src/quic_conn.c new file mode 100644 index 0000000..5233496 --- /dev/null +++ b/src/quic_conn.c @@ -0,0 +1,1893 @@ +/* + * QUIC protocol implementation. Lower layer with internal features implemented + * here such as QUIC encryption, idle timeout, acknowledgement and + * retransmission. + * + * Copyright 2020 HAProxy Technologies, Frederic Lecaille <flecaille@haproxy.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + */ + +#include <haproxy/quic_conn.h> + +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> + +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <netinet/tcp.h> + +#include <import/ebmbtree.h> + +#include <haproxy/buf-t.h> +#include <haproxy/compat.h> +#include <haproxy/api.h> +#include <haproxy/debug.h> +#include <haproxy/tools.h> +#include <haproxy/ticks.h> + +#include <haproxy/connection.h> +#include <haproxy/fd.h> +#include <haproxy/freq_ctr.h> +#include <haproxy/frontend.h> +#include <haproxy/global.h> +#include <haproxy/h3.h> +#include <haproxy/hq_interop.h> +#include <haproxy/log.h> +#include <haproxy/mux_quic.h> +#include <haproxy/ncbuf.h> +#include <haproxy/pipe.h> +#include <haproxy/proxy.h> +#include <haproxy/quic_ack.h> +#include <haproxy/quic_cc.h> +#include <haproxy/quic_cli-t.h> +#include <haproxy/quic_frame.h> +#include <haproxy/quic_enc.h> +#include <haproxy/quic_loss.h> +#include <haproxy/quic_rx.h> +#include <haproxy/quic_ssl.h> +#include <haproxy/quic_sock.h> +#include <haproxy/quic_stats.h> +#include <haproxy/quic_stream.h> +#include <haproxy/quic_tp.h> +#include <haproxy/quic_trace.h> +#include <haproxy/quic_tx.h> +#include <haproxy/cbuf.h> +#include <haproxy/proto_quic.h> +#include <haproxy/quic_tls.h> +#include <haproxy/ssl_sock.h> +#include <haproxy/task.h> +#include <haproxy/thread.h> +#include <haproxy/trace.h> + +/* list of supported QUIC versions by this implementation */ +const struct quic_version quic_versions[] = { + { + .num = QUIC_PROTOCOL_VERSION_DRAFT_29, + .initial_salt = initial_salt_draft_29, + .initial_salt_len = sizeof initial_salt_draft_29, + .key_label = (const unsigned char *)QUIC_HKDF_KEY_LABEL_V1, + .key_label_len = sizeof(QUIC_HKDF_KEY_LABEL_V1) - 1, + .iv_label = (const unsigned char *)QUIC_HKDF_IV_LABEL_V1, + .iv_label_len = sizeof(QUIC_HKDF_IV_LABEL_V1) - 1, + .hp_label = (const unsigned char *)QUIC_HKDF_HP_LABEL_V1, + .hp_label_len = sizeof(QUIC_HKDF_HP_LABEL_V1) - 1, + .ku_label = (const unsigned char *)QUIC_HKDF_KU_LABEL_V1, + .ku_label_len = sizeof(QUIC_HKDF_KU_LABEL_V1) - 1, + .retry_tag_key = (const unsigned char *)QUIC_TLS_RETRY_KEY_DRAFT, + .retry_tag_nonce = (const unsigned char *)QUIC_TLS_RETRY_NONCE_DRAFT, + }, + { + .num = QUIC_PROTOCOL_VERSION_1, + .initial_salt = initial_salt_v1, + .initial_salt_len = sizeof initial_salt_v1, + .key_label = (const unsigned char *)QUIC_HKDF_KEY_LABEL_V1, + .key_label_len = sizeof(QUIC_HKDF_KEY_LABEL_V1) - 1, + .iv_label = (const unsigned char *)QUIC_HKDF_IV_LABEL_V1, + .iv_label_len = sizeof(QUIC_HKDF_IV_LABEL_V1) - 1, + .hp_label = (const unsigned char *)QUIC_HKDF_HP_LABEL_V1, + .hp_label_len = sizeof(QUIC_HKDF_HP_LABEL_V1) - 1, + .ku_label = (const unsigned char *)QUIC_HKDF_KU_LABEL_V1, + .ku_label_len = sizeof(QUIC_HKDF_KU_LABEL_V1) - 1, + .retry_tag_key = (const unsigned char *)QUIC_TLS_RETRY_KEY_V1, + .retry_tag_nonce = (const unsigned char *)QUIC_TLS_RETRY_NONCE_V1, + }, + { + .num = QUIC_PROTOCOL_VERSION_2, + .initial_salt = initial_salt_v2, + .initial_salt_len = sizeof initial_salt_v2, + .key_label = (const unsigned char *)QUIC_HKDF_KEY_LABEL_V2, + .key_label_len = sizeof(QUIC_HKDF_KEY_LABEL_V2) - 1, + .iv_label = (const unsigned char *)QUIC_HKDF_IV_LABEL_V2, + .iv_label_len = sizeof(QUIC_HKDF_IV_LABEL_V2) - 1, + .hp_label = (const unsigned char *)QUIC_HKDF_HP_LABEL_V2, + .hp_label_len = sizeof(QUIC_HKDF_HP_LABEL_V2) - 1, + .ku_label = (const unsigned char *)QUIC_HKDF_KU_LABEL_V2, + .ku_label_len = sizeof(QUIC_HKDF_KU_LABEL_V2) - 1, + .retry_tag_key = (const unsigned char *)QUIC_TLS_RETRY_KEY_V2, + .retry_tag_nonce = (const unsigned char *)QUIC_TLS_RETRY_NONCE_V2, + }, +}; + +/* Function pointers, can be used to compute a hash from first generated CID and to derive new CIDs */ +uint64_t (*quic_hash64_from_cid)(const unsigned char *cid, int size, const unsigned char *secret, size_t secretlen) = NULL; +void (*quic_newcid_from_hash64)(unsigned char *cid, int size, uint64_t hash, const unsigned char *secret, size_t secretlen) = NULL; + +/* The total number of supported versions */ +const size_t quic_versions_nb = sizeof quic_versions / sizeof *quic_versions; +/* Listener only preferred version */ +const struct quic_version *preferred_version; +/* RFC 8999 5.4. Version + * A Version field with a + * value of 0x00000000 is reserved for version negotiation + */ +const struct quic_version quic_version_VN_reserved = { .num = 0, }; + +DECLARE_STATIC_POOL(pool_head_quic_conn, "quic_conn", sizeof(struct quic_conn)); +DECLARE_STATIC_POOL(pool_head_quic_conn_closed, "quic_conn_closed", sizeof(struct quic_conn_closed)); +DECLARE_STATIC_POOL(pool_head_quic_cids, "quic_cids", sizeof(struct eb_root)); +DECLARE_POOL(pool_head_quic_connection_id, + "quic_connection_id", sizeof(struct quic_connection_id)); + +struct task *quic_conn_app_io_cb(struct task *t, void *context, unsigned int state); +static int quic_conn_init_timer(struct quic_conn *qc); +static int quic_conn_init_idle_timer_task(struct quic_conn *qc, struct proxy *px); + +/* Returns 1 if the peer has validated <qc> QUIC connection address, 0 if not. */ +int quic_peer_validated_addr(struct quic_conn *qc) +{ + if (!qc_is_listener(qc)) + return 1; + + if (qc->flags & QUIC_FL_CONN_PEER_VALIDATED_ADDR) + return 1; + + BUG_ON(qc->bytes.prep > 3 * qc->bytes.rx); + + return 0; +} + +/* To be called to kill a connection as soon as possible (without sending any packet). */ +void qc_kill_conn(struct quic_conn *qc) +{ + TRACE_ENTER(QUIC_EV_CONN_KILL, qc); + TRACE_PROTO("killing the connection", QUIC_EV_CONN_KILL, qc); + qc->flags |= QUIC_FL_CONN_TO_KILL; + qc->flags &= ~QUIC_FL_CONN_RETRANS_NEEDED; + task_wakeup(qc->idle_timer_task, TASK_WOKEN_OTHER); + + qc_notify_err(qc); + + TRACE_LEAVE(QUIC_EV_CONN_KILL, qc); +} + +/* Set the timer attached to the QUIC connection with <ctx> as I/O handler and used for + * both loss detection and PTO and schedule the task assiated to this timer if needed. + */ +void qc_set_timer(struct quic_conn *qc) +{ + struct quic_pktns *pktns; + unsigned int pto; + int handshake_confirmed; + + TRACE_ENTER(QUIC_EV_CONN_STIMER, qc); + TRACE_PROTO("set timer", QUIC_EV_CONN_STIMER, qc, NULL, NULL, &qc->path->ifae_pkts); + + pktns = NULL; + if (!qc->timer_task) { + TRACE_PROTO("already released timer task", QUIC_EV_CONN_STIMER, qc); + goto leave; + } + + pktns = quic_loss_pktns(qc); + if (tick_isset(pktns->tx.loss_time)) { + qc->timer = pktns->tx.loss_time; + goto out; + } + + /* anti-amplification: the timer must be + * cancelled for a server which reached the anti-amplification limit. + */ + if (!quic_peer_validated_addr(qc) && + (qc->flags & QUIC_FL_CONN_ANTI_AMPLIFICATION_REACHED)) { + TRACE_PROTO("anti-amplification reached", QUIC_EV_CONN_STIMER, qc); + qc->timer = TICK_ETERNITY; + goto out; + } + + if (!qc->path->ifae_pkts && quic_peer_validated_addr(qc)) { + TRACE_PROTO("timer cancellation", QUIC_EV_CONN_STIMER, qc); + /* Timer cancellation. */ + qc->timer = TICK_ETERNITY; + goto out; + } + + handshake_confirmed = qc->state >= QUIC_HS_ST_CONFIRMED; + pktns = quic_pto_pktns(qc, handshake_confirmed, &pto); + if (tick_isset(pto)) + qc->timer = pto; + out: + if (qc->timer == TICK_ETERNITY) { + qc->timer_task->expire = TICK_ETERNITY; + } + else if (tick_is_expired(qc->timer, now_ms)) { + TRACE_DEVEL("wakeup asap timer task", QUIC_EV_CONN_STIMER, qc); + task_wakeup(qc->timer_task, TASK_WOKEN_MSG); + } + else { + TRACE_DEVEL("timer task scheduling", QUIC_EV_CONN_STIMER, qc); + task_schedule(qc->timer_task, qc->timer); + } + leave: + TRACE_PROTO("set timer", QUIC_EV_CONN_STIMER, qc, pktns); + TRACE_LEAVE(QUIC_EV_CONN_STIMER, qc); +} + +/* Prepare the emission of CONNECTION_CLOSE with error <err>. All send/receive + * activity for <qc> will be interrupted. + */ +void quic_set_connection_close(struct quic_conn *qc, const struct quic_err err) +{ + TRACE_ENTER(QUIC_EV_CONN_CLOSE, qc); + if (qc->flags & QUIC_FL_CONN_IMMEDIATE_CLOSE) + goto leave; + + TRACE_STATE("setting immediate close", QUIC_EV_CONN_CLOSE, qc); + qc->flags |= QUIC_FL_CONN_IMMEDIATE_CLOSE; + qc->err.code = err.code; + qc->err.app = err.app; + + leave: + TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); +} + +/* Set <alert> TLS alert as QUIC CRYPTO_ERROR error */ +void quic_set_tls_alert(struct quic_conn *qc, int alert) +{ + TRACE_ENTER(QUIC_EV_CONN_SSLALERT, qc); + + quic_set_connection_close(qc, quic_err_tls(alert)); + qc->flags |= QUIC_FL_CONN_TLS_ALERT; + TRACE_STATE("Alert set", QUIC_EV_CONN_SSLALERT, qc); + + TRACE_LEAVE(QUIC_EV_CONN_SSLALERT, qc); +} + +/* Set the application for <qc> QUIC connection. + * Return 1 if succeeded, 0 if not. + */ +int quic_set_app_ops(struct quic_conn *qc, const unsigned char *alpn, size_t alpn_len) +{ + if (alpn_len >= 2 && memcmp(alpn, "h3", 2) == 0) + qc->app_ops = &h3_ops; + else if (alpn_len >= 10 && memcmp(alpn, "hq-interop", 10) == 0) + qc->app_ops = &hq_interop_ops; + else + return 0; + + return 1; +} + +/* Schedule a CONNECTION_CLOSE emission on <qc> if the MUX has been released + * and all STREAM data are acknowledged. The MUX is responsible to have set + * <qc.err> before as it is reused for the CONNECTION_CLOSE frame. + * + * TODO this should also be called on lost packet detection + */ +void qc_check_close_on_released_mux(struct quic_conn *qc) +{ + TRACE_ENTER(QUIC_EV_CONN_CLOSE, qc); + + if (qc->mux_state == QC_MUX_RELEASED && eb_is_empty(&qc->streams_by_id)) { + /* Reuse errcode which should have been previously set by the MUX on release. */ + quic_set_connection_close(qc, qc->err); + tasklet_wakeup(qc->wait_event.tasklet); + } + + TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); +} + +/* Finalize <qc> QUIC connection: + + * MUST be called after having received the remote transport parameters which + * are parsed when the TLS callback for the ClientHello message is called upon + * SSL_do_handshake() calls, not necessarily at the first time as this TLS + * message may be split between packets + * Return 1 if succeeded, 0 if not. + */ +int qc_conn_finalize(struct quic_conn *qc, int server) +{ + int ret = 0; + + TRACE_ENTER(QUIC_EV_CONN_NEW, qc); + + if (qc->flags & QUIC_FL_CONN_FINALIZED) + goto finalized; + + if (!quic_tls_finalize(qc, server)) + goto out; + + /* This connection is functional (ready to send/receive) */ + qc->flags |= QUIC_FL_CONN_FINALIZED; + + finalized: + ret = 1; + out: + TRACE_LEAVE(QUIC_EV_CONN_NEW, qc); + return ret; +} + +void quic_conn_closed_err_count_inc(struct quic_conn *qc, struct quic_frame *frm) +{ + TRACE_ENTER(QUIC_EV_CONN_CLOSE, qc); + + if (frm->type == QUIC_FT_CONNECTION_CLOSE) + quic_stats_transp_err_count_inc(qc->prx_counters, frm->connection_close.error_code); + else if (frm->type == QUIC_FT_CONNECTION_CLOSE_APP) { + if (qc->mux_state != QC_MUX_READY || !qc->qcc->app_ops->inc_err_cnt) + goto out; + + qc->qcc->app_ops->inc_err_cnt(qc->qcc->ctx, frm->connection_close_app.error_code); + } + + out: + TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); +} + +/* Cancel a request on connection <qc> for stream id <id>. This is useful when + * the client opens a new stream but the MUX has already been released. A + * STOP_SENDING + RESET_STREAM frames are prepared for emission. + * + * TODO this function is closely related to H3. Its place should be in H3 layer + * instead of quic-conn but this requires an architecture adjustment. + * + * Returns 1 on success else 0. + */ +int qc_h3_request_reject(struct quic_conn *qc, uint64_t id) +{ + int ret = 0; + struct quic_frame *ss, *rs; + struct quic_enc_level *qel = qc->ael; + const uint64_t app_error_code = H3_REQUEST_REJECTED; + + TRACE_ENTER(QUIC_EV_CONN_PRSHPKT, qc); + + /* Do not emit rejection for unknown unidirectional stream as it is + * forbidden to close some of them (H3 control stream and QPACK + * encoder/decoder streams). + */ + if (quic_stream_is_uni(id)) { + ret = 1; + goto out; + } + + ss = qc_frm_alloc(QUIC_FT_STOP_SENDING); + if (!ss) { + TRACE_ERROR("failed to allocate quic_frame", QUIC_EV_CONN_PRSHPKT, qc); + goto out; + } + + ss->stop_sending.id = id; + ss->stop_sending.app_error_code = app_error_code; + + rs = qc_frm_alloc(QUIC_FT_RESET_STREAM); + if (!rs) { + TRACE_ERROR("failed to allocate quic_frame", QUIC_EV_CONN_PRSHPKT, qc); + qc_frm_free(qc, &ss); + goto out; + } + + rs->reset_stream.id = id; + rs->reset_stream.app_error_code = app_error_code; + rs->reset_stream.final_size = 0; + + LIST_APPEND(&qel->pktns->tx.frms, &ss->list); + LIST_APPEND(&qel->pktns->tx.frms, &rs->list); + ret = 1; + out: + TRACE_LEAVE(QUIC_EV_CONN_PRSHPKT, qc); + return ret; +} + +/* Remove a <qc> quic-conn from its ha_thread_ctx list. If <closing> is true, + * it will immediately be reinserted in the ha_thread_ctx quic_conns_clo list. + */ +void qc_detach_th_ctx_list(struct quic_conn *qc, int closing) +{ + struct bref *bref, *back; + + /* Detach CLI context watchers currently dumping this connection. + * Reattach them to the next quic_conn instance. + */ + list_for_each_entry_safe(bref, back, &qc->back_refs, users) { + /* Remove watcher from this quic_conn instance. */ + LIST_DEL_INIT(&bref->users); + + /* Attach it to next instance unless it was the last list element. */ + if (qc->el_th_ctx.n != &th_ctx->quic_conns && + qc->el_th_ctx.n != &th_ctx->quic_conns_clo) { + struct quic_conn *next = LIST_NEXT(&qc->el_th_ctx, + struct quic_conn *, + el_th_ctx); + LIST_APPEND(&next->back_refs, &bref->users); + } + bref->ref = qc->el_th_ctx.n; + __ha_barrier_store(); + } + + /* Remove quic_conn from global ha_thread_ctx list. */ + LIST_DEL_INIT(&qc->el_th_ctx); + + if (closing) + LIST_APPEND(&th_ctx->quic_conns_clo, &qc->el_th_ctx); +} + + +/* Copy at <pos> position a stateless reset token depending on the + * <salt> salt input. This is the cluster secret which will be derived + * as HKDF input secret to generate this token. + * Return 1 if succeeded, 0 if not. + */ +int quic_stateless_reset_token_cpy(unsigned char *pos, size_t len, + const unsigned char *salt, size_t saltlen) +{ + /* Input secret */ + const unsigned char *key = global.cluster_secret; + size_t keylen = sizeof global.cluster_secret; + /* Info */ + const unsigned char label[] = "stateless token"; + size_t labellen = sizeof label - 1; + int ret; + + ret = quic_hkdf_extract_and_expand(EVP_sha256(), pos, len, + key, keylen, salt, saltlen, label, labellen); + return ret; +} + +/* Build all the frames which must be sent just after the handshake have succeeded. + * This is essentially NEW_CONNECTION_ID frames. A QUIC server must also send + * a HANDSHAKE_DONE frame. + * Return 1 if succeeded, 0 if not. + */ +int quic_build_post_handshake_frames(struct quic_conn *qc) +{ + int ret = 0, max; + struct quic_enc_level *qel; + struct quic_frame *frm, *frmbak; + struct list frm_list = LIST_HEAD_INIT(frm_list); + struct eb64_node *node; + + TRACE_ENTER(QUIC_EV_CONN_IO_CB, qc); + + qel = qc->ael; + /* Only servers must send a HANDSHAKE_DONE frame. */ + if (qc_is_listener(qc)) { + frm = qc_frm_alloc(QUIC_FT_HANDSHAKE_DONE); + if (!frm) { + TRACE_ERROR("frame allocation error", QUIC_EV_CONN_IO_CB, qc); + goto leave; + } + + LIST_APPEND(&frm_list, &frm->list); + } + + /* Initialize <max> connection IDs minus one: there is + * already one connection ID used for the current connection. Also limit + * the number of connection IDs sent to the peer to 4 (3 from this function + * plus 1 for the current connection. + * Note that active_connection_id_limit >= 2: this has been already checked + * when receiving this parameter. + */ + max = QUIC_MIN(qc->tx.params.active_connection_id_limit - 1, (uint64_t)3); + while (max--) { + struct quic_connection_id *conn_id; + + frm = qc_frm_alloc(QUIC_FT_NEW_CONNECTION_ID); + if (!frm) { + TRACE_ERROR("frame allocation error", QUIC_EV_CONN_IO_CB, qc); + goto err; + } + + conn_id = new_quic_cid(qc->cids, qc, NULL, NULL); + if (!conn_id) { + qc_frm_free(qc, &frm); + TRACE_ERROR("CID allocation error", QUIC_EV_CONN_IO_CB, qc); + goto err; + } + + /* TODO To prevent CID tree locking, all CIDs created here + * could be allocated at the same time as the first one. + */ + quic_cid_insert(conn_id); + + quic_connection_id_to_frm_cpy(frm, conn_id); + LIST_APPEND(&frm_list, &frm->list); + } + + LIST_SPLICE(&qel->pktns->tx.frms, &frm_list); + qc->flags &= ~QUIC_FL_CONN_NEED_POST_HANDSHAKE_FRMS; + + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_IO_CB, qc); + return ret; + + err: + /* free the frames */ + list_for_each_entry_safe(frm, frmbak, &frm_list, list) + qc_frm_free(qc, &frm); + + /* The first CID sequence number value used to allocated CIDs by this function is 1, + * 0 being the sequence number of the CID for this connection. + */ + node = eb64_lookup_ge(qc->cids, 1); + while (node) { + struct quic_connection_id *conn_id; + + conn_id = eb64_entry(node, struct quic_connection_id, seq_num); + if (conn_id->seq_num.key >= max) + break; + + node = eb64_next(node); + quic_cid_delete(conn_id); + + eb64_delete(&conn_id->seq_num); + pool_free(pool_head_quic_connection_id, conn_id); + } + goto leave; +} + + +/* QUIC connection packet handler task (post handshake) */ +struct task *quic_conn_app_io_cb(struct task *t, void *context, unsigned int state) +{ + struct quic_conn *qc = context; + struct quic_enc_level *qel; + + TRACE_ENTER(QUIC_EV_CONN_IO_CB, qc); + + qel = qc->ael; + TRACE_STATE("connection handshake state", QUIC_EV_CONN_IO_CB, qc, &qc->state); + + if (qc_test_fd(qc)) + qc_rcv_buf(qc); + + /* Prepare post-handshake frames + * - after connection is instantiated (accept is done) + * - handshake state is completed (may not be the case here in 0-RTT) + */ + if ((qc->flags & QUIC_FL_CONN_NEED_POST_HANDSHAKE_FRMS) && qc->conn && + qc->state >= QUIC_HS_ST_COMPLETE) { + quic_build_post_handshake_frames(qc); + } + + /* Retranmissions */ + if (qc->flags & QUIC_FL_CONN_RETRANS_NEEDED) { + TRACE_STATE("retransmission needed", QUIC_EV_CONN_IO_CB, qc); + qc->flags &= ~QUIC_FL_CONN_RETRANS_NEEDED; + if (!qc_dgrams_retransmit(qc)) + goto out; + } + + if (!qc_treat_rx_pkts(qc)) { + TRACE_DEVEL("qc_treat_rx_pkts() failed", QUIC_EV_CONN_IO_CB, qc); + goto out; + } + + if (qc->flags & QUIC_FL_CONN_TO_KILL) { + TRACE_DEVEL("connection to be killed", QUIC_EV_CONN_IO_CB, qc); + goto out; + } + + if ((qc->flags & QUIC_FL_CONN_DRAINING) && + !(qc->flags & QUIC_FL_CONN_IMMEDIATE_CLOSE)) { + TRACE_STATE("draining connection (must not send packets)", QUIC_EV_CONN_IO_CB, qc); + goto out; + } + + /* XXX TODO: how to limit the list frames to send */ + if (!qc_send_app_pkts(qc, &qel->pktns->tx.frms)) { + TRACE_DEVEL("qc_send_app_pkts() failed", QUIC_EV_CONN_IO_CB, qc); + goto out; + } + + out: + if ((qc->flags & QUIC_FL_CONN_CLOSING) && qc->mux_state != QC_MUX_READY) { + quic_conn_release(qc); + qc = NULL; + } + + TRACE_LEAVE(QUIC_EV_CONN_IO_CB, qc); + return t; +} + +static void quic_release_cc_conn(struct quic_conn_closed *cc_qc) +{ + struct quic_conn *qc = (struct quic_conn *)cc_qc; + + TRACE_ENTER(QUIC_EV_CONN_IO_CB, cc_qc); + + task_destroy(cc_qc->idle_timer_task); + cc_qc->idle_timer_task = NULL; + tasklet_free(qc->wait_event.tasklet); + free_quic_conn_cids(qc); + pool_free(pool_head_quic_cids, cc_qc->cids); + cc_qc->cids = NULL; + pool_free(pool_head_quic_cc_buf, cc_qc->cc_buf_area); + cc_qc->cc_buf_area = NULL; + /* free the SSL sock context */ + pool_free(pool_head_quic_conn_closed, cc_qc); + + TRACE_ENTER(QUIC_EV_CONN_IO_CB); +} + +/* QUIC connection packet handler task used when in "closing connection" state. */ +static struct task *quic_conn_closed_io_cb(struct task *t, void *context, unsigned int state) +{ + struct quic_conn_closed *cc_qc = context; + struct quic_conn *qc = (struct quic_conn *)cc_qc; + struct buffer buf; + uint16_t dglen; + struct quic_tx_packet *first_pkt; + size_t headlen = sizeof dglen + sizeof first_pkt; + + TRACE_ENTER(QUIC_EV_CONN_IO_CB, qc); + + if (qc_test_fd(qc)) + qc_rcv_buf(qc); + + /* Do not send too much data if the peer address was not validated. */ + if ((qc->flags & QUIC_FL_CONN_IMMEDIATE_CLOSE) && + !(qc->flags & QUIC_FL_CONN_PEER_VALIDATED_ADDR) && + quic_may_send_bytes(qc) < cc_qc->cc_dgram_len) + goto leave; + + buf = b_make(cc_qc->cc_buf_area + headlen, + QUIC_MAX_CC_BUFSIZE - headlen, 0, cc_qc->cc_dgram_len); + if (qc_snd_buf(qc, &buf, buf.data, 0) < 0) { + TRACE_ERROR("sendto fatal error", QUIC_EV_CONN_IO_CB, qc); + quic_release_cc_conn(cc_qc); + cc_qc = NULL; + qc = NULL; + t = NULL; + goto leave; + } + + qc->flags &= ~QUIC_FL_CONN_IMMEDIATE_CLOSE; + + leave: + TRACE_LEAVE(QUIC_EV_CONN_IO_CB, qc); + + return t; +} + +/* The task handling the idle timeout of a connection in "connection close" state */ +static struct task *quic_conn_closed_idle_timer_task(struct task *t, void *ctx, unsigned int state) +{ + struct quic_conn_closed *cc_qc = ctx; + + quic_release_cc_conn(cc_qc); + + return NULL; +} + +/* Allocate a new connection in "connection close" state and return it + * if succeeded, NULL if not. This function is also responsible of + * copying enough and the least possible information from <qc> original + * connection to the newly allocated connection so that to keep it + * functional until its idle timer expires. + */ +static struct quic_conn_closed *qc_new_cc_conn(struct quic_conn *qc) +{ + struct quic_conn_closed *cc_qc; + + cc_qc = pool_alloc(pool_head_quic_conn_closed); + if (!cc_qc) + return NULL; + + quic_conn_mv_cids_to_cc_conn(cc_qc, qc); + + qc_init_fd((struct quic_conn *)cc_qc); + + cc_qc->flags = qc->flags; + cc_qc->err = qc->err; + + cc_qc->nb_pkt_for_cc = qc->nb_pkt_for_cc; + cc_qc->nb_pkt_since_cc = qc->nb_pkt_since_cc; + + cc_qc->local_addr = qc->local_addr; + cc_qc->peer_addr = qc->peer_addr; + + cc_qc->wait_event.tasklet = qc->wait_event.tasklet; + cc_qc->wait_event.tasklet->process = quic_conn_closed_io_cb; + cc_qc->wait_event.tasklet->context = cc_qc; + cc_qc->wait_event.events = 0; + cc_qc->subs = NULL; + + cc_qc->bytes.prep = qc->bytes.prep; + cc_qc->bytes.tx = qc->bytes.tx; + cc_qc->bytes.rx = qc->bytes.rx; + + cc_qc->odcid = qc->odcid; + cc_qc->dcid = qc->dcid; + cc_qc->scid = qc->scid; + + cc_qc->li = qc->li; + cc_qc->cids = qc->cids; + + cc_qc->idle_timer_task = qc->idle_timer_task; + cc_qc->idle_timer_task->process = quic_conn_closed_idle_timer_task; + cc_qc->idle_timer_task->context = cc_qc; + cc_qc->idle_expire = qc->idle_expire; + + cc_qc->conn = qc->conn; + qc->conn = NULL; + + cc_qc->cc_buf_area = qc->tx.cc_buf_area; + cc_qc->cc_dgram_len = qc->tx.cc_dgram_len; + TRACE_PRINTF(TRACE_LEVEL_PROTO, QUIC_EV_CONN_IO_CB, qc, 0, 0, 0, + "switch qc@%p to cc_qc@%p", qc, cc_qc); + + return cc_qc; +} + +/* QUIC connection packet handler task. */ +struct task *quic_conn_io_cb(struct task *t, void *context, unsigned int state) +{ + int ret; + struct quic_conn *qc = context; + struct buffer *buf = NULL; + int st; + struct tasklet *tl = (struct tasklet *)t; + + TRACE_ENTER(QUIC_EV_CONN_IO_CB, qc); + + st = qc->state; + TRACE_PROTO("connection state", QUIC_EV_CONN_IO_CB, qc, &st); + + if (HA_ATOMIC_LOAD(&tl->state) & TASK_HEAVY) { + HA_ATOMIC_AND(&tl->state, ~TASK_HEAVY); + qc_ssl_provide_all_quic_data(qc, qc->xprt_ctx); + } + + /* Retranmissions */ + if (qc->flags & QUIC_FL_CONN_RETRANS_NEEDED) { + TRACE_DEVEL("retransmission needed", QUIC_EV_CONN_PHPKTS, qc); + qc->flags &= ~QUIC_FL_CONN_RETRANS_NEEDED; + if (!qc_dgrams_retransmit(qc)) + goto out; + } + + if (qc_test_fd(qc)) + qc_rcv_buf(qc); + + if (!qc_treat_rx_pkts(qc)) + goto out; + + if (HA_ATOMIC_LOAD(&tl->state) & TASK_HEAVY) { + tasklet_wakeup(tl); + goto out; + } + + if (qc->flags & QUIC_FL_CONN_TO_KILL) { + TRACE_DEVEL("connection to be killed", QUIC_EV_CONN_PHPKTS, qc); + goto out; + } + + if ((qc->flags & QUIC_FL_CONN_DRAINING) && + !(qc->flags & QUIC_FL_CONN_IMMEDIATE_CLOSE)) + goto out; + + st = qc->state; + if (st >= QUIC_HS_ST_COMPLETE) { + if (!(qc->flags & QUIC_FL_CONN_HPKTNS_DCD)) { + /* Discard the Handshake packet number space. */ + TRACE_PROTO("discarding Handshake pktns", QUIC_EV_CONN_PHPKTS, qc); + quic_pktns_discard(qc->hel->pktns, qc); + qc_set_timer(qc); + qc_el_rx_pkts_del(qc->hel); + qc_release_pktns_frms(qc, qc->hel->pktns); + } + } + + buf = qc_get_txb(qc); + if (!buf) + goto out; + + if (b_data(buf) && !qc_purge_txbuf(qc, buf)) + goto out; + + /* Currently buf cannot be non-empty at this stage. Even if a previous + * sendto() has failed it is emptied to simulate packet emission and + * rely on QUIC lost detection to try to emit it. + */ + BUG_ON_HOT(b_data(buf)); + b_reset(buf); + + ret = qc_prep_hpkts(qc, buf, NULL); + if (ret == -1) { + qc_txb_release(qc); + goto out; + } + + if (ret && !qc_send_ppkts(buf, qc->xprt_ctx)) { + if (qc->flags & QUIC_FL_CONN_TO_KILL) + qc_txb_release(qc); + goto out; + } + + qc_txb_release(qc); + + out: + /* Release the Handshake encryption level and packet number space if + * the Handshake is confirmed and if there is no need to send + * anymore Handshake packets. + */ + if (quic_tls_pktns_is_dcd(qc, qc->hpktns) && + !qc_need_sending(qc, qc->hel)) { + /* Ensure Initial packet encryption level and packet number space have + * been released. + */ + qc_enc_level_free(qc, &qc->iel); + quic_pktns_release(qc, &qc->ipktns); + qc_enc_level_free(qc, &qc->hel); + quic_pktns_release(qc, &qc->hpktns); + /* Also release the negotiated Initial TLS context. */ + quic_nictx_free(qc); + } + + if ((qc->flags & QUIC_FL_CONN_CLOSING) && qc->mux_state != QC_MUX_READY) { + quic_conn_release(qc); + qc = NULL; + } + + TRACE_PROTO("ssl error", QUIC_EV_CONN_IO_CB, qc, &st); + TRACE_LEAVE(QUIC_EV_CONN_IO_CB, qc); + return t; +} + +/* Callback called upon loss detection and PTO timer expirations. */ +struct task *qc_process_timer(struct task *task, void *ctx, unsigned int state) +{ + struct quic_conn *qc = ctx; + struct quic_pktns *pktns; + + TRACE_ENTER(QUIC_EV_CONN_PTIMER, qc); + TRACE_PROTO("process timer", QUIC_EV_CONN_PTIMER, qc, + NULL, NULL, &qc->path->ifae_pkts); + + task->expire = TICK_ETERNITY; + pktns = quic_loss_pktns(qc); + + if (qc->flags & (QUIC_FL_CONN_DRAINING|QUIC_FL_CONN_TO_KILL)) { + TRACE_PROTO("cancelled action (draining state)", QUIC_EV_CONN_PTIMER, qc); + goto out; + } + + if (tick_isset(pktns->tx.loss_time)) { + struct list lost_pkts = LIST_HEAD_INIT(lost_pkts); + + qc_packet_loss_lookup(pktns, qc, &lost_pkts); + if (!LIST_ISEMPTY(&lost_pkts)) + tasklet_wakeup(qc->wait_event.tasklet); + if (qc_release_lost_pkts(qc, pktns, &lost_pkts, now_ms)) + qc_set_timer(qc); + goto out; + } + + if (qc->path->in_flight) { + pktns = quic_pto_pktns(qc, qc->state >= QUIC_HS_ST_CONFIRMED, NULL); + if (!pktns->tx.in_flight) { + TRACE_PROTO("No in flight packets to probe with", QUIC_EV_CONN_TXPKT, qc); + goto out; + } + + if (pktns == qc->ipktns) { + if (qc_may_probe_ipktns(qc)) { + qc->flags |= QUIC_FL_CONN_RETRANS_NEEDED; + pktns->flags |= QUIC_FL_PKTNS_PROBE_NEEDED; + TRACE_STATE("needs to probe Initial packet number space", QUIC_EV_CONN_TXPKT, qc); + } + else { + TRACE_STATE("Cannot probe Initial packet number space", QUIC_EV_CONN_TXPKT, qc); + } + if (qc->hpktns && qc->hpktns->tx.in_flight) { + qc->flags |= QUIC_FL_CONN_RETRANS_NEEDED; + qc->hpktns->flags |= QUIC_FL_PKTNS_PROBE_NEEDED; + TRACE_STATE("needs to probe Handshake packet number space", QUIC_EV_CONN_TXPKT, qc); + } + } + else if (pktns == qc->hpktns) { + TRACE_STATE("needs to probe Handshake packet number space", QUIC_EV_CONN_TXPKT, qc); + qc->flags |= QUIC_FL_CONN_RETRANS_NEEDED; + pktns->flags |= QUIC_FL_PKTNS_PROBE_NEEDED; + if (qc->ipktns && qc->ipktns->tx.in_flight) { + if (qc_may_probe_ipktns(qc)) { + qc->ipktns->flags |= QUIC_FL_PKTNS_PROBE_NEEDED; + TRACE_STATE("needs to probe Initial packet number space", QUIC_EV_CONN_TXPKT, qc); + } + else { + TRACE_STATE("Cannot probe Initial packet number space", QUIC_EV_CONN_TXPKT, qc); + } + } + } + else if (pktns == qc->apktns) { + pktns->tx.pto_probe = QUIC_MAX_NB_PTO_DGRAMS; + /* Wake up upper layer if waiting to send new data. */ + if (!qc_notify_send(qc)) { + TRACE_STATE("needs to probe 01RTT packet number space", QUIC_EV_CONN_TXPKT, qc); + qc->flags |= QUIC_FL_CONN_RETRANS_NEEDED; + pktns->flags |= QUIC_FL_PKTNS_PROBE_NEEDED; + } + } + } + else if (!qc_is_listener(qc) && qc->state <= QUIC_HS_ST_COMPLETE) { + if (quic_tls_has_tx_sec(qc->hel)) + qc->hel->pktns->tx.pto_probe = 1; + if (quic_tls_has_tx_sec(qc->iel)) + qc->iel->pktns->tx.pto_probe = 1; + } + + tasklet_wakeup(qc->wait_event.tasklet); + qc->path->loss.pto_count++; + + out: + TRACE_PROTO("process timer", QUIC_EV_CONN_PTIMER, qc, pktns); + TRACE_LEAVE(QUIC_EV_CONN_PTIMER, qc); + + return task; +} + +/* Allocate a new QUIC connection with <version> as QUIC version. <ipv4> + * boolean is set to 1 for IPv4 connection, 0 for IPv6. <server> is set to 1 + * for QUIC servers (or haproxy listeners). + * <dcid> is the destination connection ID, <scid> is the source connection ID. + * This latter <scid> CID as the same value on the wire as the one for <conn_id> + * which is the first CID of this connection but a different internal representation used to build + * NEW_CONNECTION_ID frames. This is the responsibility of the caller to insert + * <conn_id> in the CIDs tree for this connection (qc->cids). + * <token> is the token found to be used for this connection with <token_len> as + * length. Endpoints addresses are specified via <local_addr> and <peer_addr>. + * Returns the connection if succeeded, NULL if not. + */ +struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, + struct quic_cid *dcid, struct quic_cid *scid, + const struct quic_cid *token_odcid, + struct quic_connection_id *conn_id, + struct sockaddr_storage *local_addr, + struct sockaddr_storage *peer_addr, + int server, int token, void *owner) +{ + int i; + struct quic_conn *qc = NULL; + struct listener *l = server ? owner : NULL; + struct proxy *prx = l ? l->bind_conf->frontend : NULL; + struct quic_cc_algo *cc_algo = NULL; + unsigned int next_actconn = 0, next_sslconn = 0, next_handshake = 0; + + TRACE_ENTER(QUIC_EV_CONN_INIT); + + next_actconn = increment_actconn(); + if (!next_actconn) { + _HA_ATOMIC_INC(&maxconn_reached); + TRACE_STATE("maxconn reached", QUIC_EV_CONN_INIT); + goto err; + } + + next_sslconn = increment_sslconn(); + if (!next_sslconn) { + TRACE_STATE("sslconn reached", QUIC_EV_CONN_INIT); + goto err; + } + + if (server) { + next_handshake = quic_increment_curr_handshake(l); + if (!next_handshake) { + TRACE_STATE("max handshake reached", QUIC_EV_CONN_INIT); + goto err; + } + } + + qc = pool_alloc(pool_head_quic_conn); + if (!qc) { + TRACE_ERROR("Could not allocate a new connection", QUIC_EV_CONN_INIT); + goto err; + } + + /* Now that quic_conn instance is allocated, quic_conn_release() will + * ensure global accounting is decremented. + */ + next_handshake = next_sslconn = next_actconn = 0; + + /* Initialize in priority qc members required for a safe dealloc. */ + qc->nictx = NULL; + /* Prevents these CID to be dumped by TRACE() calls */ + qc->scid.len = qc->odcid.len = qc->dcid.len = 0; + /* required to use MTLIST_IN_LIST */ + MT_LIST_INIT(&qc->accept_list); + + LIST_INIT(&qc->rx.pkt_list); + + qc->streams_by_id = EB_ROOT_UNIQUE; + + /* Required to call free_quic_conn_cids() from quic_conn_release() */ + qc->cids = NULL; + qc->tx.cc_buf_area = NULL; + qc_init_fd(qc); + + LIST_INIT(&qc->back_refs); + LIST_INIT(&qc->el_th_ctx); + + qc->wait_event.tasklet = NULL; + + /* Required to destroy <qc> tasks from quic_conn_release() */ + qc->timer_task = NULL; + qc->idle_timer_task = NULL; + + qc->xprt_ctx = NULL; + qc->conn = NULL; + qc->qcc = NULL; + qc->app_ops = NULL; + qc->path = NULL; + + /* Keyupdate: required to safely call quic_tls_ku_free() from + * quic_conn_release(). + */ + quic_tls_ku_reset(&qc->ku.prv_rx); + quic_tls_ku_reset(&qc->ku.nxt_rx); + quic_tls_ku_reset(&qc->ku.nxt_tx); + + /* Encryption levels */ + qc->iel = qc->eel = qc->hel = qc->ael = NULL; + LIST_INIT(&qc->qel_list); + /* Packet number spaces */ + qc->ipktns = qc->hpktns = qc->apktns = NULL; + LIST_INIT(&qc->pktns_list); + + /* Required to safely call quic_conn_prx_cntrs_update() from quic_conn_release(). */ + qc->prx_counters = NULL; + + /* QUIC Server (or listener). */ + if (server) { + cc_algo = l->bind_conf->quic_cc_algo; + + qc->prx_counters = EXTRA_COUNTERS_GET(prx->extra_counters_fe, + &quic_stats_module); + qc->flags = QUIC_FL_CONN_LISTENER; + qc->state = QUIC_HS_ST_SERVER_INITIAL; + /* Copy the client original DCID. */ + qc->odcid = *dcid; + /* Copy the packet SCID to reuse it as DCID for sending */ + qc->dcid = *scid; + qc->tx.buf = BUF_NULL; + qc->li = l; + } + /* QUIC Client (outgoing connection to servers) */ + else { + qc->state = QUIC_HS_ST_CLIENT_INITIAL; + if (dcid->len) + memcpy(qc->dcid.data, dcid->data, dcid->len); + qc->dcid.len = dcid->len; + qc->li = NULL; + } + qc->mux_state = QC_MUX_NULL; + qc->err = quic_err_transport(QC_ERR_NO_ERROR); + + /* If connection is instantiated due to an INITIAL packet with an + * already checked token, consider the peer address as validated. + */ + if (token_odcid->len) { + TRACE_STATE("validate peer address due to initial token", + QUIC_EV_CONN_INIT, qc); + qc->flags |= QUIC_FL_CONN_PEER_VALIDATED_ADDR; + } + else { + HA_ATOMIC_INC(&qc->prx_counters->half_open_conn); + } + + /* Now proceeds to allocation of qc members. */ + qc->rx.buf.area = pool_alloc(pool_head_quic_conn_rxbuf); + if (!qc->rx.buf.area) { + TRACE_ERROR("Could not allocate a new RX buffer", QUIC_EV_CONN_INIT, qc); + goto err; + } + + qc->cids = pool_alloc(pool_head_quic_cids); + if (!qc->cids) { + TRACE_ERROR("Could not allocate a new CID tree", QUIC_EV_CONN_INIT, qc); + goto err; + } + *qc->cids = EB_ROOT; + + conn_id->qc = qc; + + if (HA_ATOMIC_LOAD(&l->rx.quic_mode) == QUIC_SOCK_MODE_CONN && + (global.tune.options & GTUNE_QUIC_SOCK_PER_CONN) && + is_addr(local_addr)) { + TRACE_USER("Allocate a socket for QUIC connection", QUIC_EV_CONN_INIT, qc); + qc_alloc_fd(qc, local_addr, peer_addr); + + /* haproxy soft-stop is supported only for QUIC connections + * with their owned socket. + */ + if (qc_test_fd(qc)) + _HA_ATOMIC_INC(&jobs); + } + + /* Select our SCID which is the first CID with 0 as sequence number. */ + qc->scid = conn_id->cid; + + if (!qc_enc_level_alloc(qc, &qc->ipktns, &qc->iel, ssl_encryption_initial)) { + TRACE_ERROR("Could not initialize an encryption level", QUIC_EV_CONN_INIT, qc); + goto err; + } + + qc->original_version = qv; + qc->negotiated_version = NULL; + qc->tps_tls_ext = (qc->original_version->num & 0xff000000) == 0xff000000 ? + TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS_DRAFT: + TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS; + /* TX part. */ + qc->bytes.tx = qc->bytes.prep = 0; + memset(&qc->tx.params, 0, sizeof(qc->tx.params)); + qc->tx.buf = BUF_NULL; + qc->tx.cc_buf = BUF_NULL; + qc->tx.cc_buf_area = NULL; + qc->tx.cc_dgram_len = 0; + /* RX part. */ + qc->bytes.rx = 0; + memset(&qc->rx.params, 0, sizeof(qc->rx.params)); + qc->rx.buf = b_make(qc->rx.buf.area, QUIC_CONN_RX_BUFSZ, 0, 0); + for (i = 0; i < QCS_MAX_TYPES; i++) + qc->rx.strms[i].nb_streams = 0; + + qc->nb_pkt_for_cc = 1; + qc->nb_pkt_since_cc = 0; + + if (!quic_tls_ku_init(qc)) { + TRACE_ERROR("Key update initialization failed", QUIC_EV_CONN_INIT, qc); + goto err; + } + + qc->max_ack_delay = 0; + /* Only one path at this time (multipath not supported) */ + qc->path = &qc->paths[0]; + quic_cc_path_init(qc->path, ipv4, server ? l->bind_conf->max_cwnd : 0, + cc_algo ? cc_algo : default_quic_cc_algo, qc); + + qc->stream_buf_count = 0; + memcpy(&qc->local_addr, local_addr, sizeof(qc->local_addr)); + memcpy(&qc->peer_addr, peer_addr, sizeof qc->peer_addr); + + if (server && !qc_lstnr_params_init(qc, &l->bind_conf->quic_params, + conn_id->stateless_reset_token, + dcid->data, dcid->len, + qc->scid.data, qc->scid.len, token_odcid)) + goto err; + + /* Initialize the idle timeout of the connection at the "max_idle_timeout" + * value from local transport parameters. + */ + qc->max_idle_timeout = qc->rx.params.max_idle_timeout; + qc->wait_event.tasklet = tasklet_new(); + if (!qc->wait_event.tasklet) { + TRACE_ERROR("tasklet_new() failed", QUIC_EV_CONN_TXPKT); + goto err; + } + qc->wait_event.tasklet->process = quic_conn_io_cb; + qc->wait_event.tasklet->context = qc; + qc->wait_event.events = 0; + qc->subs = NULL; + + if (qc_alloc_ssl_sock_ctx(qc) || + !quic_conn_init_timer(qc) || + !quic_conn_init_idle_timer_task(qc, prx)) + goto err; + + if (!qc_new_isecs(qc, &qc->iel->tls_ctx, qc->original_version, dcid->data, dcid->len, 1)) + goto err; + + /* Counters initialization */ + memset(&qc->cntrs, 0, sizeof qc->cntrs); + + LIST_APPEND(&th_ctx->quic_conns, &qc->el_th_ctx); + qc->qc_epoch = HA_ATOMIC_LOAD(&qc_epoch); + + TRACE_LEAVE(QUIC_EV_CONN_INIT, qc); + + return qc; + + err: + quic_conn_release(qc); + + /* Decrement global counters. Done only for errors happening before or + * on pool_head_quic_conn alloc. All other cases are covered by + * quic_conn_release(). + */ + if (next_actconn) + _HA_ATOMIC_DEC(&actconn); + if (next_sslconn) + _HA_ATOMIC_DEC(&global.sslconns); + if (next_handshake) + _HA_ATOMIC_DEC(&l->rx.quic_curr_handshake); + + TRACE_LEAVE(QUIC_EV_CONN_INIT); + return NULL; +} + +/* React to a connection migration initiated on <qc> by a client with the new + * path addresses <peer_addr>/<local_addr>. + * + * Returns 0 on success else non-zero. + */ +int qc_handle_conn_migration(struct quic_conn *qc, + const struct sockaddr_storage *peer_addr, + const struct sockaddr_storage *local_addr) +{ + TRACE_ENTER(QUIC_EV_CONN_LPKT, qc); + + /* RFC 9000. Connection Migration + * + * If the peer sent the disable_active_migration transport parameter, + * an endpoint also MUST NOT send packets (including probing packets; + * see Section 9.1) from a different local address to the address the peer + * used during the handshake, unless the endpoint has acted on a + * preferred_address transport parameter from the peer. + */ + if (qc->li->bind_conf->quic_params.disable_active_migration) { + TRACE_ERROR("Active migration was disabled, datagram dropped", QUIC_EV_CONN_LPKT, qc); + goto err; + } + + /* RFC 9000 9. Connection Migration + * + * The design of QUIC relies on endpoints retaining a stable address for + * the duration of the handshake. An endpoint MUST NOT initiate + * connection migration before the handshake is confirmed, as defined in + * Section 4.1.2 of [QUIC-TLS]. + */ + if (qc->state < QUIC_HS_ST_COMPLETE) { + TRACE_STATE("Connection migration during handshake rejected", QUIC_EV_CONN_LPKT, qc); + goto err; + } + + /* RFC 9000 9. Connection Migration + * + * TODO + * An endpoint MUST + * perform path validation (Section 8.2) if it detects any change to a + * peer's address, unless it has previously validated that address. + */ + + /* Update quic-conn owned socket if in used. + * TODO try to reuse it instead of closing and opening a new one. + */ + if (qc_test_fd(qc)) { + /* TODO try to reuse socket instead of closing it and opening a new one. */ + TRACE_STATE("Connection migration detected, allocate a new connection socket", QUIC_EV_CONN_LPKT, qc); + qc_release_fd(qc, 1); + /* TODO need to adjust <jobs> on socket allocation failure. */ + qc_alloc_fd(qc, local_addr, peer_addr); + } + + qc->local_addr = *local_addr; + qc->peer_addr = *peer_addr; + qc->cntrs.conn_migration_done++; + + TRACE_LEAVE(QUIC_EV_CONN_LPKT, qc); + return 0; + + err: + TRACE_LEAVE(QUIC_EV_CONN_LPKT, qc); + return 1; +} + + +/* Update the proxy counters of <qc> QUIC connection from its counters */ +static inline void quic_conn_prx_cntrs_update(struct quic_conn *qc) +{ + if (!qc->prx_counters) + return; + + HA_ATOMIC_ADD(&qc->prx_counters->dropped_pkt, qc->cntrs.dropped_pkt); + HA_ATOMIC_ADD(&qc->prx_counters->dropped_pkt_bufoverrun, qc->cntrs.dropped_pkt_bufoverrun); + HA_ATOMIC_ADD(&qc->prx_counters->dropped_parsing, qc->cntrs.dropped_parsing); + HA_ATOMIC_ADD(&qc->prx_counters->socket_full, qc->cntrs.socket_full); + HA_ATOMIC_ADD(&qc->prx_counters->sendto_err, qc->cntrs.sendto_err); + HA_ATOMIC_ADD(&qc->prx_counters->sendto_err_unknown, qc->cntrs.sendto_err_unknown); + HA_ATOMIC_ADD(&qc->prx_counters->sent_pkt, qc->cntrs.sent_pkt); + /* It is possible that ->path was not initialized. For instance if a + * QUIC connection allocation has failed. + */ + if (qc->path) + HA_ATOMIC_ADD(&qc->prx_counters->lost_pkt, qc->path->loss.nb_lost_pkt); + HA_ATOMIC_ADD(&qc->prx_counters->conn_migration_done, qc->cntrs.conn_migration_done); + /* Stream related counters */ + HA_ATOMIC_ADD(&qc->prx_counters->data_blocked, qc->cntrs.data_blocked); + HA_ATOMIC_ADD(&qc->prx_counters->stream_data_blocked, qc->cntrs.stream_data_blocked); + HA_ATOMIC_ADD(&qc->prx_counters->streams_blocked_bidi, qc->cntrs.streams_blocked_bidi); + HA_ATOMIC_ADD(&qc->prx_counters->streams_blocked_uni, qc->cntrs.streams_blocked_uni); +} + +/* Release the quic_conn <qc>. The connection is removed from the CIDs tree. + * The connection tasklet is killed. + * + * This function must only be called by the thread responsible of the quic_conn + * tasklet. + */ +void quic_conn_release(struct quic_conn *qc) +{ + struct eb64_node *node; + struct quic_rx_packet *pkt, *pktback; + struct quic_conn_closed *cc_qc; + + TRACE_ENTER(QUIC_EV_CONN_CLOSE, qc); + + if (!qc) + goto leave; + + /* We must not free the quic-conn if the MUX is still allocated. */ + BUG_ON(qc->mux_state == QC_MUX_READY); + + cc_qc = NULL; + if ((qc->flags & QUIC_FL_CONN_CLOSING) && !(qc->flags & QUIC_FL_CONN_EXP_TIMER) && + qc->tx.cc_buf_area) + cc_qc = qc_new_cc_conn(qc); + + if (!cc_qc) { + task_destroy(qc->idle_timer_task); + qc->idle_timer_task = NULL; + tasklet_free(qc->wait_event.tasklet); + /* remove the connection from receiver cids trees */ + free_quic_conn_cids(qc); + pool_free(pool_head_quic_cids, qc->cids); + qc->cids = NULL; + pool_free(pool_head_quic_cc_buf, qc->tx.cc_buf_area); + qc->tx.cc_buf_area = NULL; + } + + if (qc_test_fd(qc)) + _HA_ATOMIC_DEC(&jobs); + + /* Close quic-conn socket fd. */ + qc_release_fd(qc, 0); + + /* in the unlikely (but possible) case the connection was just added to + * the accept_list we must delete it from there. + */ + if (MT_LIST_INLIST(&qc->accept_list)) { + MT_LIST_DELETE(&qc->accept_list); + BUG_ON(qc->li->rx.quic_curr_accept == 0); + HA_ATOMIC_DEC(&qc->li->rx.quic_curr_accept); + } + + /* free remaining stream descriptors */ + node = eb64_first(&qc->streams_by_id); + while (node) { + struct qc_stream_desc *stream; + + stream = eb64_entry(node, struct qc_stream_desc, by_id); + node = eb64_next(node); + + /* all streams attached to the quic-conn are released, so + * qc_stream_desc_free will liberate the stream instance. + */ + BUG_ON(!stream->release); + qc_stream_desc_free(stream, 1); + } + + /* free the SSL sock context */ + qc_free_ssl_sock_ctx(&qc->xprt_ctx); + /* Purge Rx packet list. */ + list_for_each_entry_safe(pkt, pktback, &qc->rx.pkt_list, qc_rx_pkt_list) { + LIST_DELETE(&pkt->qc_rx_pkt_list); + pool_free(pool_head_quic_rx_packet, pkt); + } + + task_destroy(qc->timer_task); + qc->timer_task = NULL; + + quic_tls_ku_free(qc); + if (qc->ael) { + struct quic_tls_ctx *actx = &qc->ael->tls_ctx; + + /* Secrets used by keyupdate */ + pool_free(pool_head_quic_tls_secret, actx->rx.secret); + pool_free(pool_head_quic_tls_secret, actx->tx.secret); + } + + qc_enc_level_free(qc, &qc->iel); + qc_enc_level_free(qc, &qc->eel); + qc_enc_level_free(qc, &qc->hel); + qc_enc_level_free(qc, &qc->ael); + + quic_tls_ctx_free(&qc->nictx); + + quic_pktns_release(qc, &qc->ipktns); + quic_pktns_release(qc, &qc->hpktns); + quic_pktns_release(qc, &qc->apktns); + + qc_detach_th_ctx_list(qc, 0); + + quic_conn_prx_cntrs_update(qc); + pool_free(pool_head_quic_conn_rxbuf, qc->rx.buf.area); + qc->rx.buf.area = NULL; + + /* Connection released before peer address validated. */ + if (unlikely(!(qc->flags & QUIC_FL_CONN_PEER_VALIDATED_ADDR))) { + BUG_ON(!qc->prx_counters->half_open_conn); + HA_ATOMIC_DEC(&qc->prx_counters->half_open_conn); + } + + /* Connection released before handshake completion. */ + if (unlikely(qc->state < QUIC_HS_ST_COMPLETE)) { + if (qc_is_listener(qc)) { + BUG_ON(qc->li->rx.quic_curr_handshake == 0); + HA_ATOMIC_DEC(&qc->li->rx.quic_curr_handshake); + } + } + + pool_free(pool_head_quic_conn, qc); + qc = NULL; + + /* Decrement global counters when quic_conn is deallocated. + * quic_conn_closed instances are not accounted as they run for a short + * time with limited resources. + */ + _HA_ATOMIC_DEC(&actconn); + _HA_ATOMIC_DEC(&global.sslconns); + + TRACE_PROTO("QUIC conn. freed", QUIC_EV_CONN_FREED, qc); + leave: + TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); +} + +/* Initialize the timer task of <qc> QUIC connection. + * Returns 1 if succeeded, 0 if not. + */ +static int quic_conn_init_timer(struct quic_conn *qc) +{ + int ret = 0; + /* Attach this task to the same thread ID used for the connection */ + TRACE_ENTER(QUIC_EV_CONN_NEW, qc); + + qc->timer_task = task_new_here(); + if (!qc->timer_task) { + TRACE_ERROR("timer task allocation failed", QUIC_EV_CONN_NEW, qc); + goto leave; + } + + qc->timer = TICK_ETERNITY; + qc->timer_task->process = qc_process_timer; + qc->timer_task->context = qc; + + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_NEW, qc); + return ret; +} + +/* Rearm the idle timer or the ack timer (if not already armde) for <qc> QUIC + * connection. */ +void qc_idle_timer_do_rearm(struct quic_conn *qc, int arm_ack) +{ + unsigned int expire; + + /* It is possible the idle timer task has been already released. */ + if (!qc->idle_timer_task) + return; + + if (qc->flags & (QUIC_FL_CONN_CLOSING|QUIC_FL_CONN_DRAINING)) { + /* RFC 9000 10.2. Immediate Close + * + * The closing and draining connection states exist to ensure that + * connections close cleanly and that delayed or reordered packets are + * properly discarded. These states SHOULD persist for at least three + * times the current PTO interval as defined in [QUIC-RECOVERY]. + */ + + /* Delay is limited to 1s which should cover most of network + * conditions. The process should not be impacted by a + * connection with a high RTT. + */ + expire = MIN(3 * quic_pto(qc), 1000); + } + else { + /* RFC 9000 10.1. Idle Timeout + * + * To avoid excessively small idle timeout periods, endpoints MUST + * increase the idle timeout period to be at least three times the + * current Probe Timeout (PTO). This allows for multiple PTOs to expire, + * and therefore multiple probes to be sent and lost, prior to idle + * timeout. + */ + expire = QUIC_MAX(3 * quic_pto(qc), qc->max_idle_timeout); + } + + qc->idle_expire = tick_add(now_ms, MS_TO_TICKS(expire)); + /* Note that the ACK timer is not armed during the handshake. So, + * the handshake expiration date is taken into an account only + * when <arm_ack> is false. + */ + if (arm_ack) { + /* Arm the ack timer only if not already armed. */ + if (!tick_isset(qc->ack_expire)) { + qc->ack_expire = tick_add(now_ms, MS_TO_TICKS(QUIC_ACK_DELAY)); + qc->idle_timer_task->expire = qc->ack_expire; + task_queue(qc->idle_timer_task); + TRACE_PROTO("ack timer armed", QUIC_EV_CONN_IDLE_TIMER, qc); + } + } + else { + qc->idle_timer_task->expire = tick_first(qc->ack_expire, qc->idle_expire); + if (qc->state < QUIC_HS_ST_COMPLETE) + qc->idle_timer_task->expire = tick_first(qc->hs_expire, qc->idle_expire); + task_queue(qc->idle_timer_task); + TRACE_PROTO("idle timer armed", QUIC_EV_CONN_IDLE_TIMER, qc); + } +} + +/* Rearm the idle timer or ack timer for <qc> QUIC connection depending on <read> + * and <arm_ack> booleans. The former is set to 1 when receiving a packet , + * and 0 when sending packet. <arm_ack> is set to 1 if this is the ack timer + * which must be rearmed. + */ +void qc_idle_timer_rearm(struct quic_conn *qc, int read, int arm_ack) +{ + TRACE_ENTER(QUIC_EV_CONN_IDLE_TIMER, qc); + + if (read) { + qc->flags |= QUIC_FL_CONN_IDLE_TIMER_RESTARTED_AFTER_READ; + } + else { + qc->flags &= ~QUIC_FL_CONN_IDLE_TIMER_RESTARTED_AFTER_READ; + } + qc_idle_timer_do_rearm(qc, arm_ack); + + leave: + TRACE_LEAVE(QUIC_EV_CONN_IDLE_TIMER, qc); +} + +/* The task handling the idle timeout */ +struct task *qc_idle_timer_task(struct task *t, void *ctx, unsigned int state) +{ + struct quic_conn *qc = ctx; + + TRACE_ENTER(QUIC_EV_CONN_IDLE_TIMER, qc); + + if ((state & TASK_WOKEN_ANY) == TASK_WOKEN_TIMER && !tick_is_expired(t->expire, now_ms)) + goto requeue; + + if (tick_is_expired(qc->ack_expire, now_ms)) { + TRACE_PROTO("ack timer expired", QUIC_EV_CONN_IDLE_TIMER, qc); + qc->ack_expire = TICK_ETERNITY; + /* Note that ->idle_expire is always set. */ + t->expire = qc->idle_expire; + /* Do not wakeup the I/O handler in DRAINING state or if the + * connection must be killed as soon as possible. + */ + if (!(qc->flags & (QUIC_FL_CONN_DRAINING|QUIC_FL_CONN_TO_KILL))) { + qc->flags |= QUIC_FL_CONN_ACK_TIMER_FIRED; + tasklet_wakeup(qc->wait_event.tasklet); + } + + goto requeue; + } + + TRACE_PROTO("idle timer task running", QUIC_EV_CONN_IDLE_TIMER, qc); + /* Notify the MUX before settings QUIC_FL_CONN_EXP_TIMER or the MUX + * might free the quic-conn too early via quic_close(). + */ + qc_notify_err(qc); + + /* If the MUX is still alive, keep the quic-conn. The MUX is + * responsible to call quic_close to release it. + */ + qc->flags |= QUIC_FL_CONN_EXP_TIMER; + if (qc->mux_state != QC_MUX_READY) { + quic_conn_release(qc); + qc = NULL; + } + else { + task_destroy(t); + qc->idle_timer_task = NULL; + } + + t = NULL; + + /* TODO if the quic-conn cannot be freed because of the MUX, we may at + * least clean some parts of it such as the tasklet. + */ + + requeue: + TRACE_LEAVE(QUIC_EV_CONN_IDLE_TIMER, qc); + return t; +} + +/* Initialize the idle timeout task for <qc>. + * Returns 1 if succeeded, 0 if not. + */ +static int quic_conn_init_idle_timer_task(struct quic_conn *qc, + struct proxy *px) +{ + int ret = 0; + int timeout; + + TRACE_ENTER(QUIC_EV_CONN_NEW, qc); + + + timeout = px->timeout.client_hs ? px->timeout.client_hs : px->timeout.client; + qc->idle_timer_task = task_new_here(); + if (!qc->idle_timer_task) { + TRACE_ERROR("Idle timer task allocation failed", QUIC_EV_CONN_NEW, qc); + goto leave; + } + + qc->idle_timer_task->process = qc_idle_timer_task; + qc->idle_timer_task->context = qc; + qc->ack_expire = TICK_ETERNITY; + qc->hs_expire = tick_add_ifset(now_ms, MS_TO_TICKS(timeout)); + qc_idle_timer_rearm(qc, 1, 0); + task_queue(qc->idle_timer_task); + + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_NEW, qc); + return ret; +} + +/* Return the QUIC version (quic_version struct) with <version> as version number + * if supported or NULL if not. + */ +const struct quic_version *qc_supported_version(uint32_t version) +{ + int i; + + if (unlikely(!version)) + return &quic_version_VN_reserved; + + for (i = 0; i < quic_versions_nb; i++) + if (quic_versions[i].num == version) + return &quic_versions[i]; + + return NULL; +} + +/* Check if connection ID <dcid> of length <dcid_len> belongs to <qc> local + * CIDs. This can be used to determine if a datagram is addressed to the right + * connection instance. + * + * Returns a boolean value. + */ +int qc_check_dcid(struct quic_conn *qc, unsigned char *dcid, size_t dcid_len) +{ + const uchar idx = _quic_cid_tree_idx(dcid); + struct quic_connection_id *conn_id; + struct ebmb_node *node = NULL; + struct quic_cid_tree *tree = &quic_cid_trees[idx]; + + /* Test against our default CID or client ODCID. */ + if ((qc->scid.len == dcid_len && + memcmp(qc->scid.data, dcid, dcid_len) == 0) || + (qc->odcid.len == dcid_len && + memcmp(qc->odcid.data, dcid, dcid_len) == 0)) { + return 1; + } + + /* Test against our other CIDs. This can happen if the client has + * decided to switch to a new one. + * + * TODO to avoid locking, loop through qc.cids as an alternative. + * + * TODO set it to our default CID to avoid this operation next time. + */ + HA_RWLOCK_RDLOCK(QC_CID_LOCK, &tree->lock); + node = ebmb_lookup(&tree->root, dcid, dcid_len); + HA_RWLOCK_RDUNLOCK(QC_CID_LOCK, &tree->lock); + + if (node) { + conn_id = ebmb_entry(node, struct quic_connection_id, node); + if (qc == conn_id->qc) + return 1; + } + + return 0; +} + +/* Wake-up upper layer for sending if all conditions are met : + * - room in congestion window or probe packet to sent + * - socket FD ready to sent or listener socket used + * + * Returns 1 if upper layer has been woken up else 0. + */ +int qc_notify_send(struct quic_conn *qc) +{ + const struct quic_pktns *pktns = qc->apktns; + + if (qc->subs && qc->subs->events & SUB_RETRY_SEND) { + /* RFC 9002 7.5. Probe Timeout + * + * Probe packets MUST NOT be blocked by the congestion controller. + */ + if ((quic_cc_path_prep_data(qc->path) || pktns->tx.pto_probe) && + (!qc_test_fd(qc) || !fd_send_active(qc->fd))) { + tasklet_wakeup(qc->subs->tasklet); + qc->subs->events &= ~SUB_RETRY_SEND; + if (!qc->subs->events) + qc->subs = NULL; + + return 1; + } + } + + return 0; +} + +/* Notify upper layer of a fatal error which forces to close the connection. */ +void qc_notify_err(struct quic_conn *qc) +{ + TRACE_ENTER(QUIC_EV_CONN_CLOSE, qc); + + if (qc->mux_state == QC_MUX_READY) { + TRACE_STATE("error notified to mux", QUIC_EV_CONN_CLOSE, qc); + + /* Mark socket as closed. */ + qc->conn->flags |= CO_FL_ERROR | CO_FL_SOCK_RD_SH | CO_FL_SOCK_WR_SH; + + /* TODO quic-conn layer must stay active until MUX is released. + * Thus, we have to wake up directly to ensure upper stream + * layer will be notified of the error. If a proper separation + * is made between MUX and quic-conn layer, wake up could be + * conducted only with qc.subs. + */ + tasklet_wakeup(qc->qcc->wait_event.tasklet); + } + + TRACE_LEAVE(QUIC_EV_CONN_CLOSE, qc); +} + +/* Move a <qc> QUIC connection and its resources from the current thread to the + * new one <new_tid> optionally in association with <new_li> (since it may need + * to change when migrating to a thread from a different group, otherwise leave + * it NULL). After this call, the connection cannot be dereferenced anymore on + * the current thread. + * + * Returns 0 on success else non-zero. + */ +int qc_set_tid_affinity(struct quic_conn *qc, uint new_tid, struct listener *new_li) +{ + struct task *t1 = NULL, *t2 = NULL; + struct tasklet *t3 = NULL; + + struct quic_connection_id *conn_id; + struct eb64_node *node; + + TRACE_ENTER(QUIC_EV_CONN_SET_AFFINITY, qc); + + /* Pre-allocate all required resources. This ensures we do not left a + * connection with only some of its field rebinded. + */ + if (((t1 = task_new_on(new_tid)) == NULL) || + (qc->timer_task && (t2 = task_new_on(new_tid)) == NULL) || + (t3 = tasklet_new()) == NULL) { + goto err; + } + + /* Reinit idle timer task. */ + task_kill(qc->idle_timer_task); + t1->expire = qc->idle_timer_task->expire; + qc->idle_timer_task = t1; + qc->idle_timer_task->process = qc_idle_timer_task; + qc->idle_timer_task->context = qc; + + /* Reinit timer task if allocated. */ + if (qc->timer_task) { + task_kill(qc->timer_task); + qc->timer_task = t2; + qc->timer_task->process = qc_process_timer; + qc->timer_task->context = qc; + } + + /* Reinit IO tasklet. */ + if (qc->wait_event.tasklet->state & TASK_IN_LIST) + qc->flags |= QUIC_FL_CONN_IO_TO_REQUEUE; + tasklet_kill(qc->wait_event.tasklet); + /* In most cases quic_conn_app_io_cb is used but for 0-RTT quic_conn_io_cb can be still activated. */ + t3->process = qc->wait_event.tasklet->process; + qc->wait_event.tasklet = t3; + qc->wait_event.tasklet->tid = new_tid; + qc->wait_event.tasklet->context = qc; + qc->wait_event.events = 0; + + /* Rebind the connection FD. */ + if (qc_test_fd(qc)) { + /* Reading is reactivated by the new thread. */ + fd_migrate_on(qc->fd, new_tid); + } + + /* Remove conn from per-thread list instance. It will be hidden from + * "show quic" until rebinding is completed. + */ + qc_detach_th_ctx_list(qc, 0); + + node = eb64_first(qc->cids); + BUG_ON(!node || eb64_next(node)); /* One and only one CID must be present before affinity rebind. */ + conn_id = eb64_entry(node, struct quic_connection_id, seq_num); + + /* At this point no connection was accounted for yet on this + * listener so it's OK to just swap the pointer. + */ + if (new_li && new_li != qc->li) + qc->li = new_li; + + /* Rebinding is considered done when CID points to the new thread. No + * access should be done to quic-conn instance after it. + */ + qc->flags |= QUIC_FL_CONN_AFFINITY_CHANGED; + HA_ATOMIC_STORE(&conn_id->tid, new_tid); + qc = NULL; + + TRACE_LEAVE(QUIC_EV_CONN_SET_AFFINITY, NULL); + return 0; + + err: + task_destroy(t1); + task_destroy(t2); + tasklet_free(t3); + + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_SET_AFFINITY, qc); + return 1; +} + +/* Must be called after qc_set_tid_affinity() on the new thread. */ +void qc_finalize_affinity_rebind(struct quic_conn *qc) +{ + TRACE_ENTER(QUIC_EV_CONN_SET_AFFINITY, qc); + + /* This function must not be called twice after an affinity rebind. */ + BUG_ON(!(qc->flags & QUIC_FL_CONN_AFFINITY_CHANGED)); + qc->flags &= ~QUIC_FL_CONN_AFFINITY_CHANGED; + + /* If quic_conn is closing it is unnecessary to migrate it as it will + * be soon released. Besides, special care must be taken for CLOSING + * connections (using quic_conn_closed and th_ctx.quic_conns_clo list for + * instance). This should never occur as CLOSING connections are + * skipped by quic_sock_accept_conn(). + */ + BUG_ON(qc->flags & (QUIC_FL_CONN_CLOSING|QUIC_FL_CONN_DRAINING)); + + /* Reinsert connection in ha_thread_ctx global list. */ + LIST_APPEND(&th_ctx->quic_conns, &qc->el_th_ctx); + qc->qc_epoch = HA_ATOMIC_LOAD(&qc_epoch); + + /* Reactivate FD polling if connection socket is active. */ + qc_want_recv(qc); + + /* Reactivate timer task if needed. */ + qc_set_timer(qc); + + /* Idle timer task is always active. */ + task_queue(qc->idle_timer_task); + + /* Reactivate IO tasklet if needed. */ + if (qc->flags & QUIC_FL_CONN_IO_TO_REQUEUE) { + tasklet_wakeup(qc->wait_event.tasklet); + qc->flags &= ~QUIC_FL_CONN_IO_TO_REQUEUE; + } + + TRACE_LEAVE(QUIC_EV_CONN_SET_AFFINITY, qc); +} + +/* + * Local variables: + * c-indent-level: 8 + * c-basic-offset: 8 + * End: + */ |