From: Frederic Lecaille Date: Wed, 25 Jun 2025 16:11:24 +0000 (+0200) Subject: MINOR: quic-be: address validation support implementation (RETRY) X-Git-Tag: v3.3-dev2~12 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=194e3bc2d5ba46c00c750d6e6bf8bf1cd19b7608;p=thirdparty%2Fhaproxy.git MINOR: quic-be: address validation support implementation (RETRY) - Add ->retry_token and ->retry_token_len new quic_conn struct members to store the retry tokens. These objects are allocated by quic_rx_packet_parse() and released by quic_conn_release(). - Add new pool for these tokens. - Implement quic_retry_packet_check() to check the integrity tag of these tokens upon RETRY packets receipt. quic_tls_generate_retry_integrity_tag() is called by this new function. It has been modified to pass the address where the tag must be generated - Add new parameter to quic_pktns_discard(). This function is called to discard the packet number spaces where the already TX packets and frames are attached to. allows the caller to prevent this function to release the in flight TX packets/frames. The frames are requeued to be resent. - Modify quic_rx_pkt_parse() to handle the RETRY packets. What must be done upon such packets receipt is: - store the retry token, - store the new peer SCID as the DCID of the connection. Note that the peer will modify again its SCID. This is why this SCID is also stored as the ODCID which must be matched with the peer retry_source_connection_id transport parameter, - discard the Initial packet number space without flagging it as discarded and prevent retransmissions calling qc_set_timer(), - modify the TLS cryptographic cipher contexts (RX/TX), - wakeup the I/O handler to send new Initial packets asap. - Modify quic_transport_param_decode() to handle the retry_source_connection_id transport parameter as a QUIC client. Then its caller is modified to check this transport parameter matches with the SCID sent by the peer with the RETRY packet. --- diff --git a/include/haproxy/quic_conn-t.h b/include/haproxy/quic_conn-t.h index a832038db..0b7fac7ec 100644 --- a/include/haproxy/quic_conn-t.h +++ b/include/haproxy/quic_conn-t.h @@ -356,6 +356,10 @@ struct quic_conn { */ uint64_t hash64; + /* QUIC client only retry token received from servers RETRY packet */ + unsigned char *retry_token; + size_t retry_token_len; + /* Initial encryption level */ struct quic_enc_level *iel; /* 0-RTT encryption level */ diff --git a/include/haproxy/quic_retry.h b/include/haproxy/quic_retry.h index d31be02ee..f3ae371d3 100644 --- a/include/haproxy/quic_retry.h +++ b/include/haproxy/quic_retry.h @@ -13,6 +13,8 @@ #include #include +extern struct pool_head *pool_head_quic_retry_token; + struct listener; int quic_generate_retry_token(unsigned char *token, size_t len, @@ -28,6 +30,9 @@ int quic_retry_token_check(struct quic_rx_packet *pkt, struct listener *l, struct quic_conn *qc, struct quic_cid *odcid); +int quic_retry_packet_check(struct quic_conn *qc, struct quic_rx_packet *pkt, + const unsigned char *beg, const unsigned char *end, + const unsigned char *pos, size_t *retry_token_len); #endif /* USE_QUIC */ #endif /* _HAPROXY_QUIC_RETRY_H */ diff --git a/include/haproxy/quic_tls.h b/include/haproxy/quic_tls.h index d2e56a89a..0e053a71e 100644 --- a/include/haproxy/quic_tls.h +++ b/include/haproxy/quic_tls.h @@ -74,7 +74,8 @@ int quic_tls_decrypt(unsigned char *buf, size_t len, const unsigned char *key, const unsigned char *iv); int quic_tls_generate_retry_integrity_tag(unsigned char *odcid, unsigned char odcid_len, - unsigned char *buf, size_t len, + const unsigned char *buf, size_t len, + unsigned char *tag, const struct quic_version *qv); int quic_tls_derive_keys(const QUIC_AEAD *aead, const EVP_CIPHER *hp, @@ -536,7 +537,8 @@ static inline int quic_pktns_init(struct quic_conn *qc, struct quic_pktns **p) return 1; } -static inline void quic_pktns_tx_pkts_release(struct quic_pktns *pktns, struct quic_conn *qc) +static inline void quic_pktns_tx_pkts_release(struct quic_pktns *pktns, + struct quic_conn *qc, int resend) { struct eb64_node *node; @@ -557,7 +559,11 @@ static inline void quic_pktns_tx_pkts_release(struct quic_pktns *pktns, struct q qc_frm_unref(frm, qc); LIST_DEL_INIT(&frm->list); quic_tx_packet_refdec(frm->pkt); - qc_frm_free(qc, &frm); + if (!resend) + qc_frm_free(qc, &frm); + else + LIST_APPEND(&pktns->tx.frms, &frm->list); + } eb64_delete(&pkt->pn_node); quic_tx_packet_refdec(pkt); @@ -572,9 +578,12 @@ static inline void quic_pktns_tx_pkts_release(struct quic_pktns *pktns, struct q * connection. * Note that all the non acknowledged TX packets and their frames are freed. * Always succeeds. + * boolean must be 1 to resend the frames which are in flight. + * This is only used to resend the Initial packet frames upon a RETRY + * packet receipt (backend only option). */ static inline void quic_pktns_discard(struct quic_pktns *pktns, - struct quic_conn *qc) + struct quic_conn *qc, int resend) { TRACE_ENTER(QUIC_EV_CONN_PHPKTS, qc); @@ -590,7 +599,7 @@ static inline void quic_pktns_discard(struct quic_pktns *pktns, pktns->tx.loss_time = TICK_ETERNITY; pktns->tx.pto_probe = 0; pktns->tx.in_flight = 0; - quic_pktns_tx_pkts_release(pktns, qc); + quic_pktns_tx_pkts_release(pktns, qc, resend); TRACE_LEAVE(QUIC_EV_CONN_PHPKTS, qc); } diff --git a/src/quic_conn.c b/src/quic_conn.c index 600d2893c..904ca37f3 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -841,7 +842,7 @@ struct task *quic_conn_io_cb(struct task *t, void *context, unsigned int state) if (discard_hpktns) { /* Discard the Handshake packet number space. */ TRACE_PROTO("discarding Handshake pktns", QUIC_EV_CONN_PHPKTS, qc); - quic_pktns_discard(qc->hel->pktns, qc); + quic_pktns_discard(qc->hel->pktns, qc, 0); qc_set_timer(qc); qc_el_rx_pkts_del(qc->hel); qc_release_pktns_frms(qc, qc->hel->pktns); @@ -921,7 +922,7 @@ struct task *quic_conn_io_cb(struct task *t, void *context, unsigned int state) qc->hpktns && qc->hpktns->tx.in_flight > 0) { /* Discard the Initial packet number space. */ TRACE_PROTO("discarding Initial pktns", QUIC_EV_CONN_PRSHPKT, qc); - quic_pktns_discard(qc->ipktns, qc); + quic_pktns_discard(qc->ipktns, qc, 0); qc_set_timer(qc); qc_el_rx_pkts_del(qc->iel); qc_release_pktns_frms(qc, qc->ipktns); @@ -1167,6 +1168,8 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, #ifdef HAVE_OPENSSL_QUIC qc->prot_level = OSSL_RECORD_PROTECTION_LEVEL_NONE; #endif + qc->retry_token = NULL; + qc->retry_token_len = 0; /* Encryption levels */ qc->iel = qc->eel = qc->hel = qc->ael = NULL; LIST_INIT(&qc->qel_list); @@ -1198,8 +1201,6 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, qc->flags = QUIC_FL_CONN_PEER_VALIDATED_ADDR; qc->state = QUIC_HS_ST_CLIENT_INITIAL; - memset(&qc->odcid, 0, sizeof qc->odcid); - qc->odcid.len = 0; /* This is the original connection ID from the peer server * point of view. */ @@ -1208,6 +1209,9 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, qc->dcid.len = sizeof(qc->dcid.data); + memcpy(&qc->odcid, qc->dcid.data, sizeof(qc->dcid.data)); + qc->odcid.len = qc->dcid.len; + conn_cid = new_quic_cid(qc->cids, qc, NULL, NULL); if (!conn_cid) goto err; @@ -1576,6 +1580,9 @@ int quic_conn_release(struct quic_conn *qc) pool_free(pool_head_quic_tls_secret, actx->tx.secret); } + pool_free(pool_head_quic_retry_token, qc->retry_token); + qc->retry_token = NULL; + qc->retry_token_len = 0; qc_enc_level_free(qc, &qc->iel); qc_enc_level_free(qc, &qc->eel); qc_enc_level_free(qc, &qc->hel); diff --git a/src/quic_retry.c b/src/quic_retry.c index 78ef88a76..9cdf69af5 100644 --- a/src/quic_retry.c +++ b/src/quic_retry.c @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,9 @@ /* Salt length used to derive retry token secret */ #define QUIC_RETRY_TOKEN_SALTLEN 16 /* bytes */ +#define QUIC_RETRY_TOKEN_MAXLEN 256 /* bytes */ + +struct pool_head *pool_head_quic_retry_token; /* Copy socket address data into buffer. * This is the responsibility of the caller to check the output buffer is big @@ -319,4 +323,71 @@ int quic_retry_token_check(struct quic_rx_packet *pkt, goto leave; } +/* QUIC client only function. + * Check the integrity tag of retry packet received on connection + * with and as packet payload delimiters. is the retry token position. + * Return 1 if succeeded, 0 if not. + */ +int quic_retry_packet_check(struct quic_conn *qc, struct quic_rx_packet *pkt, + const unsigned char *beg, const unsigned char *end, + const unsigned char *pos, size_t *retry_token_len) +{ + int ret = 0; + unsigned char tag[QUIC_TLS_TAG_LEN]; + ssize_t toklen; + + TRACE_ENTER(QUIC_EV_CONN_SPKT, qc); + + if (!pkt->version) { + TRACE_PROTO("retry packet without version", QUIC_EV_CONN_SPKT); + goto err; + } + + if (end - beg <= QUIC_LONG_PACKET_MINLEN + + pkt->scid.len + pkt->dcid.len + QUIC_TLS_TAG_LEN || + end - pos <= QUIC_TLS_TAG_LEN) { + TRACE_PROTO("Too short retry packet", QUIC_EV_CONN_SPKT); + goto err; + } + + if (!quic_tls_generate_retry_integrity_tag(qc->odcid.data, qc->odcid.len, + beg, end - beg - QUIC_TLS_TAG_LEN, + tag, pkt->version)) { + TRACE_PROTO("retry integrity tag faild", QUIC_EV_CONN_SPKT, qc); + goto err; + } + + if (memcmp(tag, end - QUIC_TLS_TAG_LEN, QUIC_TLS_TAG_LEN) != 0) { + TRACE_PROTO("retry integrity tag mismatch", QUIC_EV_CONN_SPKT, qc); + goto err; + } + + toklen = end - pos - QUIC_TLS_TAG_LEN; + if (toklen <= 0 || toklen > QUIC_RETRY_TOKEN_MAXLEN) { + TRACE_PROTO("wrong retry token size", QUIC_EV_CONN_SPKT, qc); + goto err; + } + + *retry_token_len = (size_t)toklen; + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_SPKT, qc); + return ret; + err: + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_SPKT, qc); + goto leave; +} + +static int create_quic_retry_token_pool(void) +{ + pool_head_quic_retry_token = + create_pool("quic_retry_token", QUIC_RETRY_TOKEN_MAXLEN, MEM_F_SHARED|MEM_F_EXACT); + if (!pool_head_quic_retry_token) { + ha_warning("error on QUIC retry token buffer pool allocation.\n"); + return ERR_FATAL|ERR_ABORT; + } + + return ERR_NONE; +} +REGISTER_POST_CHECK(create_quic_retry_token_pool); diff --git a/src/quic_rx.c b/src/quic_rx.c index f6903291e..9a61c8e9a 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -1191,7 +1191,7 @@ static int qc_parse_pkt_frms(struct quic_conn *qc, struct quic_rx_packet *pkt, if (qc->ipktns && !quic_tls_pktns_is_dcd(qc, qc->ipktns)) { /* Discard the handshake packet number space. */ TRACE_PROTO("discarding Initial pktns", QUIC_EV_CONN_PRSHPKT, qc); - quic_pktns_discard(qc->ipktns, qc); + quic_pktns_discard(qc->ipktns, qc, 0); qc_set_timer(qc); qc_el_rx_pkts_del(qc->iel); qc_release_pktns_frms(qc, qc->ipktns); @@ -1967,8 +1967,7 @@ static int quic_rx_pkt_parse(struct quic_conn *qc, struct quic_rx_packet *pkt, } /* Retry of Version Negotiation packets are only sent by servers */ - if (pkt->type == QUIC_PACKET_TYPE_RETRY || - (pkt->version && !pkt->version->num)) { + if (l && (pkt->type == QUIC_PACKET_TYPE_RETRY || (pkt->version && !pkt->version->num))) { TRACE_PROTO("Packet dropped", QUIC_EV_CONN_LPKT); goto drop; } @@ -1987,10 +1986,10 @@ static int quic_rx_pkt_parse(struct quic_conn *qc, struct quic_rx_packet *pkt, goto drop_silent; } - /* For Initial packets, and for servers (QUIC clients connections), - * there is no Initial connection IDs storage. - */ if (pkt->type == QUIC_PACKET_TYPE_INITIAL) { + /* For Initial packets, and for servers (QUIC clients connections), + * there is no Initial connection IDs storage. + */ uint64_t token_len; if (!quic_dec_int(&token_len, (const unsigned char **)&pos, end) || @@ -2011,6 +2010,47 @@ static int quic_rx_pkt_parse(struct quic_conn *qc, struct quic_rx_packet *pkt, pkt->token_len = token_len; pos += pkt->token_len; } + else if (pkt->type == QUIC_PACKET_TYPE_RETRY) { + if (!quic_retry_packet_check(qc, pkt, beg, end, pos, &qc->retry_token_len)) + /* TODO: should close the connection? */ + goto drop; + + qc->retry_token = pool_alloc(pool_head_quic_retry_token); + if (!qc->retry_token) { + TRACE_ERROR("retry token allocation failed", QUIC_EV_CONN_LPKT); + } + else { + memcpy(qc->retry_token, pos, qc->retry_token_len); + /* Save the peer Retry source connection ID into the connection ODCID. + * This is also this connection DCID (or even the first ODCID value). + * It can be erased because used only to check the retry integrity + * tag. Then, it will be matched against the retry_source_connection_id + * transport parameter which will be sent by the server. + */ + memcpy(qc->odcid.data, pkt->scid.data, pkt->scid.len); + qc->odcid.len = pkt->scid.len; + /* Copy the peer scid to be the destination of the next Initial packet */ + memcpy(qc->dcid.data, pkt->scid.data, pkt->scid.len); + qc->dcid.len = pkt->scid.len; + /* Initial packet number space discarding without releasing + * the existing frames (not already sent). + */ + quic_pktns_discard(qc->ipktns, qc, 1); + qc_set_timer(qc); + qc_el_rx_pkts_del(qc->iel); + /* Reset the DISCARDED flag for Initial packet number space */ + qc->flags &= ~QUIC_FL_CONN_IPKTNS_DCD; + /* Change the Initial TLS cryptographic context */ + quic_tls_ctx_secs_free(&qc->iel->tls_ctx); + if (!qc_new_isecs(qc, &qc->iel->tls_ctx, qc->original_version, + qc->dcid.data, qc->dcid.len, !!l)) + goto drop_silent; + + tasklet_wakeup(qc->wait_event.tasklet); + } + + goto drop_silent; + } else if (pkt->type != QUIC_PACKET_TYPE_0RTT) { if (pkt->dcid.len != QUIC_HAP_CID_LEN) { TRACE_PROTO("Packet dropped", diff --git a/src/quic_tls.c b/src/quic_tls.c index 7dd348a47..30a323d2e 100644 --- a/src/quic_tls.c +++ b/src/quic_tls.c @@ -95,7 +95,7 @@ void quic_pktns_release(struct quic_conn *qc, struct quic_pktns **pktns) if (!*pktns) return; - quic_pktns_tx_pkts_release(*pktns, qc); + quic_pktns_tx_pkts_release(*pktns, qc, 0); qc_release_pktns_frms(qc, *pktns); quic_free_arngs(qc, &(*pktns)->rx.arngs); LIST_DEL_INIT(&(*pktns)->list); @@ -1005,14 +1005,15 @@ int quic_tls_derive_token_secret(const EVP_MD *md, } /* Generate the AEAD tag for the Retry packet of bytes and - * write it to . The tag is written just after the area. It should + * write it to . The tag is written at address. It should * be at least 16 bytes longs. is the CID of the Initial packet * received which triggers the Retry. * * Returns non-zero on success else zero. */ int quic_tls_generate_retry_integrity_tag(unsigned char *odcid, unsigned char odcid_len, - unsigned char *pkt, size_t pkt_len, + const unsigned char *pkt, size_t pkt_len, + unsigned char *tag, const struct quic_version *qv) { const EVP_CIPHER *evp = EVP_aes_128_gcm(); @@ -1020,8 +1021,6 @@ int quic_tls_generate_retry_integrity_tag(unsigned char *odcid, unsigned char od /* encryption buffer - not used as only AEAD tag generation is proceed */ unsigned char *out = NULL; - /* address to store the AEAD tag */ - unsigned char *tag = pkt + pkt_len; int outlen, ret = 0; ctx = EVP_CIPHER_CTX_new(); diff --git a/src/quic_tp.c b/src/quic_tp.c index 4b081e2c4..fb47aa790 100644 --- a/src/quic_tp.c +++ b/src/quic_tp.c @@ -400,7 +400,13 @@ quic_transport_param_decode(struct quic_transport_params *p, int server, if (!server) return QUIC_TP_DEC_ERR_INVAL; - /* TODO implement parsing for client side */ + if (len > sizeof p->retry_source_connection_id.data) + return QUIC_TP_DEC_ERR_TRUNC; + + if (len) + memcpy(p->retry_source_connection_id.data, *buf, len); + p->retry_source_connection_id.len = len; + *buf += len; break; default: *buf += len; @@ -753,6 +759,12 @@ int quic_transport_params_store(struct quic_conn *qc, int server, return 0; } + if (server && (qc->odcid.len != tx_params->retry_source_connection_id.len || + memcmp(qc->odcid.data, tx_params->retry_source_connection_id.data, qc->odcid.len) != 0)) { + TRACE_ERROR("retry_source_connection_id mismatch", QUIC_EV_TRANSP_PARAMS, qc); + return 0; + } + /* Update the connection from transport parameters received */ if (tx_params->version_information.negotiated_version && tx_params->version_information.negotiated_version != qc->original_version) diff --git a/src/quic_tx.c b/src/quic_tx.c index 8ce67bbf6..8888fa77c 100644 --- a/src/quic_tx.c +++ b/src/quic_tx.c @@ -1304,7 +1304,7 @@ int send_retry(int fd, struct sockaddr_storage *addr, /* token integrity tag */ if ((sizeof(buf) - i < QUIC_TLS_TAG_LEN) || !quic_tls_generate_retry_integrity_tag(pkt->dcid.data, - pkt->dcid.len, buf, i, qv)) { + pkt->dcid.len, buf, i, buf + i, qv)) { TRACE_ERROR("quic_tls_generate_retry_integrity_tag() failed", QUIC_EV_CONN_TXPKT); goto out; } @@ -1809,10 +1809,14 @@ static int qc_do_build_pkt(unsigned char *pos, const unsigned char *end, /* Encode the token length (0) for an Initial packet. */ if (pkt->type == QUIC_PACKET_TYPE_INITIAL) { - if (end <= pos) + if (!quic_enc_int(&pos, end, qc->retry_token_len) || + end - pos <= qc->retry_token_len) goto no_room; - *pos++ = 0; + if (qc->retry_token_len) { + memcpy(pos, qc->retry_token, qc->retry_token_len); + pos += qc->retry_token_len; + } } head_len = pos - beg;