]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: quic-be: address validation support implementation (RETRY)
authorFrederic Lecaille <flecaille@haproxy.com>
Wed, 25 Jun 2025 16:11:24 +0000 (18:11 +0200)
committerFrederic Lecaille <flecaille@haproxy.com>
Thu, 26 Jun 2025 07:48:00 +0000 (09:48 +0200)
- 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 <pool_head_quic_retry_token> 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 <resend> 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. <resend> 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.

include/haproxy/quic_conn-t.h
include/haproxy/quic_retry.h
include/haproxy/quic_tls.h
src/quic_conn.c
src/quic_retry.c
src/quic_rx.c
src/quic_tls.c
src/quic_tp.c
src/quic_tx.c

index a832038dba767ac6aef1323f45bad0ba8a424a27..0b7fac7ec3564e2d5ea1d1b963ae29abedf83f45 100644 (file)
@@ -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 */
index d31be02eef061bdbd4fe22e97e0fc685d4c56ec5..f3ae371d353bbbe911fa520af3cf22cd0c3ca30d 100644 (file)
@@ -13,6 +13,8 @@
 #include <haproxy/quic_rx-t.h>
 #include <haproxy/quic_sock-t.h>
 
+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 */
index d2e56a89a0c10dd25038e42d552300e68f76b1d8..0e053a71e1e3bb72fab4e9ed3922e0a81d3944c8 100644 (file)
@@ -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.
+ * <resend> 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);
 }
index 600d2893caf08aaf73e1cf427b9ce9376448dd03..904ca37f3c045f37fd67a4f4f830888ecd4fcef0 100644 (file)
@@ -53,6 +53,7 @@
 #include <haproxy/quic_enc.h>
 #include <haproxy/quic_loss.h>
 #include <haproxy/quic_rx.h>
+#include <haproxy/quic_retry.h>
 #include <haproxy/quic_ssl.h>
 #include <haproxy/quic_sock.h>
 #include <haproxy/quic_stats.h>
@@ -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);
index 78ef88a76900711ea859e7e20b87d0e36534f57f..9cdf69af5a6e6c490c01305c4a22d56627daa564 100644 (file)
@@ -1,6 +1,7 @@
 #include <string.h>
 
 #include <haproxy/clock.h>
+#include <haproxy/errors.h>
 #include <haproxy/global.h>
 #include <haproxy/quic_retry.h>
 #include <haproxy/quic_tls.h>
@@ -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 <saddr> socket address data into <buf> 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 <pkt> retry packet received on <qc> connection
+ * with <beg> and <end> as packet payload delimiters. <pos> 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);
index f6903291ec5a3456f38383adaea15eea7be6142e..9a61c8e9ac3f98b07e57f34a4286230f1ba9a2cc 100644 (file)
@@ -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",
index 7dd348a4793c94d0f73b5e697c8188e1a07480bf..30a323d2ea39c326f1f1a8894267d7ee56aad06c 100644 (file)
@@ -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 <pkt> of <pkt_len> bytes and
- * write it to <tag>. The tag is written just after the <pkt> area. It should
+ * write it to <tag>. The tag is written at <tag> address. It should
  * be at least 16 bytes longs. <odcid> 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();
index 4b081e2c46c12207fafec284be7167fa1dab6a93..fb47aa7909d07e2778e2d4096d960f79435726d3 100644 (file)
@@ -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)
index 8ce67bbf622e38233dbb113c5d85ba57f843cc44..8888fa77c0664a1d6fc1352cbda861d89c4aa834 100644 (file)
@@ -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;