From: Yeonggi Kim Date: Thu, 15 Oct 2026 23:45:54 +0000 (+0900) Subject: BUG/MEDIUM: quic: use datagram destination as source for Retry X-Git-Tag: v3.5-dev10~182 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=a0ddcac11b5417dbdd6004ef6b418acd7e7a95a0;p=haproxy.git BUG/MEDIUM: quic: use datagram destination as source for Retry When a QUIC listener is bound on a wildcard address, the local address targeted by the client is retrieved on reception via IP_PKTINFO or equivalent. It is then used as source address for all datagrams of the connection, either by binding the connection socket on it, or via ancillary data when the listener socket is used. Without it, the kernel selects the source address from the route to the client, which may differ from the one targeted by the client. This is typically the case when several addresses are assigned to the host, for example VIPs configured on a loopback interface behind a load balancer in direct routing mode. As QUIC clients usually rely on connected UDP sockets, datagrams from another address are then silently dropped by their kernel. However, Retry packets are emitted before any connection exists, via send_retry() which used a bare sendto() on the listener socket without source address. In the above conditions, the client never receives the Retry and keeps retransmitting its Initial packet until it gives up. Retry is emitted in particular when quic-force-retry is set on the bind line, when a quic-initial send-retry rule matches, or when the number of half-open connections reaches tune.quic.fe.sec.retry-threshold, so handshakes may also fail only intermittently under load. Apart from not using Retry at all, which removes client address validation, the usual workaround is to explicitly bind each address, in addition to or instead of the wildcard one. Fix this by adding a source address parameter to send_retry(), before the destination one. Its callers set it to the datagram destination address, and emission now relies on quic_lstnr_sock_send(), which only uses it if the listener is bound on a wildcard address. Apart from the source address, the emission is unchanged. Also fix the trace emitted on send failure, which was a copy of the previous one. This should fix github issue #3503. This should be backported up to 2.8. Note that it relies on the two previous patches: - MINOR: quic: use the listener for Retry and Version Negotiation emission - MINOR: quic: define a function to emit a datagram on a listener socket Prior to 3.4, quic_dgram addresses are of sockaddr_storage type, so the casts are not necessary. Prior to 2.9, send_retry() is a static function defined in quic_conn.c. --- diff --git a/include/haproxy/quic_tx.h b/include/haproxy/quic_tx.h index f68fbc26b..807ff5716 100644 --- a/include/haproxy/quic_tx.h +++ b/include/haproxy/quic_tx.h @@ -47,7 +47,8 @@ int qc_send(struct quic_conn *qc, int old_data, struct list *send_list, int qc_dgrams_retransmit(struct quic_conn *qc); void qc_prep_hdshk_fast_retrans(struct quic_conn *qc, struct list *ifrms, struct list *hfrms); -int send_retry(struct listener *l, struct sockaddr_storage *dst, +int send_retry(struct listener *l, struct sockaddr_storage *src, + struct sockaddr_storage *dst, struct quic_rx_packet *pkt, const struct quic_version *qv); int send_stateless_reset(struct listener *l, struct sockaddr_storage *dstaddr, struct quic_rx_packet *rxpkt); diff --git a/src/quic_rx.c b/src/quic_rx.c index fbefac588..8005d727a 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -1845,7 +1845,9 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt, /* Validate the token, retry or not only when connection is unknown. */ if (!quic_token_validate(pkt, dgram, l, qc, &token_odcid)) { if (dgram->flags & QUIC_DGRAM_FL_SEND_RETRY) { - if (send_retry(l, (struct sockaddr_storage *)&dgram->saddr, pkt, pkt->version)) { + if (send_retry(l, (struct sockaddr_storage *)&dgram->daddr, + (struct sockaddr_storage *)&dgram->saddr, + pkt, pkt->version)) { TRACE_ERROR("Error during Retry generation", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); } @@ -1875,7 +1877,9 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt, TRACE_PROTO("Initial without token, sending retry", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); - if (send_retry(l, (struct sockaddr_storage *)&dgram->saddr, pkt, pkt->version)) { + if (send_retry(l, (struct sockaddr_storage *)&dgram->daddr, + (struct sockaddr_storage *)&dgram->saddr, + pkt, pkt->version)) { TRACE_ERROR("Error during Retry generation", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); goto out; diff --git a/src/quic_tx.c b/src/quic_tx.c index 7339c0a83..bf57571b3 100644 --- a/src/quic_tx.c +++ b/src/quic_tx.c @@ -1306,17 +1306,18 @@ static inline int quic_pkt_type(int type, uint32_t version) /* Generate a Retry packet and send it on listener socket to in - * response to the Initial packet. + * response to the Initial packet. is the local address targeted by + * the peer, used as datagram source if possible (see quic_lstnr_sock_send()). * * Returns 0 on success else non-zero. */ -int send_retry(struct listener *l, struct sockaddr_storage *dst, +int send_retry(struct listener *l, struct sockaddr_storage *src, + struct sockaddr_storage *dst, struct quic_rx_packet *pkt, const struct quic_version *qv) { int ret = 0; unsigned char buf[128]; int i = 0, token_len; - const socklen_t addrlen = get_addr_len(dst); struct quic_cid scid; TRACE_ENTER(QUIC_EV_CONN_TXPKT); @@ -1365,8 +1366,8 @@ int send_retry(struct listener *l, struct sockaddr_storage *dst, i += QUIC_TLS_TAG_LEN; - if (sendto(l->rx.fd, buf, i, 0, (struct sockaddr *)dst, addrlen) < 0) { - TRACE_ERROR("quic_tls_generate_retry_integrity_tag() failed", QUIC_EV_CONN_TXPKT); + if (quic_lstnr_sock_send(l, buf, i, src, dst) < 0) { + TRACE_ERROR("Retry emission failed", QUIC_EV_CONN_TXPKT); goto out; }