From: Yeonggi Kim Date: Thu, 15 Oct 2026 23:45:55 +0000 (+0900) Subject: BUG/MINOR: quic: use datagram destination as source for stateless reset X-Git-Tag: v3.5-dev10~181 X-Git-Url: https://git.kaiwu.me/$%7BISSUES%7D/static/gitweb.js?a=commitdiff_plain;h=f633d891786dfca7580f48e966b8017094746ad6;p=haproxy.git BUG/MINOR: quic: use datagram destination as source for stateless reset When a QUIC listener is bound on a wildcard address, the source address of emitted datagrams must be the local address targeted by the client, else the kernel selects it from the route to the client. Both may differ when several addresses are assigned to the host, for example VIPs configured on a loopback interface behind a load balancer in direct routing mode. In this case, QUIC clients relying on a connected UDP socket silently drop the datagram. Stateless reset packets are emitted without connection, via send_stateless_reset() which used a bare sendto() on the listener socket without source address. In the above conditions, the client never receives the stateless reset. This typically happens after a restart of haproxy with a configured cluster-secret, for clients which still use connection IDs of the previous process: instead of being notified immediately, they only give up after their idle timeout. Fix this by adding a source address parameter to send_stateless_reset(), before the destination one, similarly to the previous patch for Retry. Its caller sets 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. This should fix github issue #3503. This should be backported up to 2.8. Note that it relies on the first two patches of the series: - 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_stateless_reset() is a static function defined in quic_conn.c. --- diff --git a/include/haproxy/quic_tx.h b/include/haproxy/quic_tx.h index 807ff5716..97ed7b471 100644 --- a/include/haproxy/quic_tx.h +++ b/include/haproxy/quic_tx.h @@ -50,7 +50,8 @@ void qc_prep_hdshk_fast_retrans(struct quic_conn *qc, 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, +int send_stateless_reset(struct listener *l, struct sockaddr_storage *srcaddr, + struct sockaddr_storage *dstaddr, struct quic_rx_packet *rxpkt); int send_version_negotiation(struct listener *l, struct sockaddr_storage *dst, struct quic_rx_packet *pkt); diff --git a/src/quic_rx.c b/src/quic_rx.c index 8005d727a..705f32928 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -1961,7 +1961,8 @@ static struct quic_conn *quic_rx_pkt_retrieve_conn(struct quic_rx_packet *pkt, * emits stateless_reset_token in its TPs. */ TRACE_PROTO("RX non Initial pkt without connection", QUIC_EV_CONN_LPKT, NULL, NULL, NULL, pkt->version); - if (!send_stateless_reset(l, (struct sockaddr_storage *)&dgram->saddr, pkt)) + if (!send_stateless_reset(l, (struct sockaddr_storage *)&dgram->daddr, + (struct sockaddr_storage *)&dgram->saddr, pkt)) TRACE_ERROR("stateless reset not sent", QUIC_EV_CONN_LPKT, qc); goto err; } diff --git a/src/quic_tx.c b/src/quic_tx.c index bf57571b3..b966c032a 100644 --- a/src/quic_tx.c +++ b/src/quic_tx.c @@ -1204,15 +1204,17 @@ static THREAD_LOCAL struct freq_ctr quic_stateless_reset_freq_ctr; static THREAD_LOCAL uint quic_stateless_reset_rate; /* Send a stateless reset packet depending on RX packet information - * from listener socket to + * from listener socket to . is the local address + * targeted by the peer, used as datagram source if possible (see + * quic_lstnr_sock_send()). * Return 1 if succeeded, 0 if not. */ -int send_stateless_reset(struct listener *l, struct sockaddr_storage *dstaddr, +int send_stateless_reset(struct listener *l, struct sockaddr_storage *srcaddr, + struct sockaddr_storage *dstaddr, struct quic_rx_packet *rxpkt) { int ret = 0, pktlen, rndlen; unsigned char pkt[64]; - const socklen_t addrlen = get_addr_len(dstaddr); struct proxy *prx; struct quic_counters *prx_counters; @@ -1271,7 +1273,7 @@ int send_stateless_reset(struct listener *l, struct sockaddr_storage *dstaddr, rxpkt->dcid.data, rxpkt->dcid.len)) goto leave; - if (sendto(l->rx.fd, pkt, pktlen, 0, (struct sockaddr *)dstaddr, addrlen) < 0) + if (quic_lstnr_sock_send(l, pkt, pktlen, srcaddr, dstaddr) < 0) goto leave; ret = 1;