From: Frederic Lecaille Date: Thu, 20 Aug 2026 16:23:18 +0000 (+0200) Subject: MINOR: quic: add tune.quic.sec.stateless-reset-rate X-Git-Tag: v3.5-dev6~84 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=297321e3e;p=haproxy.git MINOR: quic: add tune.quic.sec.stateless-reset-rate RFC 9000 10.3 allows sending a Stateless Reset for any packet that can't be matched to a connection. A flood of garbage UDP datagrams can turn this into an unwitting amplifier, wasting CPU and bandwidth on replies. Add tune.quic.sec.stateless-reset-rate to cap the total number of Stateless Reset packets sent per second across all threads (default: 1000). It is not scoped to the frontend side: a Stateless Reset can be sent for any unmatched packet, regardless of which side received it. Each thread keeps its own THREAD_LOCAL freq_ctr and gets its own share of the total, computed once at thread startup: thread gets (N + tid) / nbthread, so the shares always add up to exactly N (no rounding overshoot. Tested by flooding a listener with short-header packets carrying random DCIDs: the number of replies received matches the configured rate rather than the number of packets sent, with normal traffic unaffected when well under the limit. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index cfe1f397a..89a088511 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1972,6 +1972,7 @@ The following keywords are supported in the "global" section : - tune.quic.fe.tx.udp-gso - tune.quic.listen - tune.quic.mem.tx-max + - tune.quic.sec.stateless-reset-rate - tune.quic.zero-copy-fwd-send - tune.renice.runtime - tune.renice.startup @@ -5364,6 +5365,18 @@ tune.quic.mem.tx-max connection will always be able to use a window of at least 2 datagrams, so a proper maxconn should be used in conjunction. +tune.quic.sec.stateless-reset-rate + Sets the maximum total number of QUIC Stateless Reset packets emitted per + second, across all threads. Each thread gets its own even share of this + total, so the actual number sent per second may be slightly under or over + depending on how evenly it divides by the thread count, but the + total across all threads never drifts from it over time. + + The default value is 1000. + + See https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 for more + information about QUIC Stateless Reset. + tune.quic.zero-copy-fwd-send { on | off } Enables ('on') of disabled ('off') the zero-copy sends of data for the QUIC multiplexer. It is enabled by default. diff --git a/include/haproxy/quic_tune-t.h b/include/haproxy/quic_tune-t.h index 56caab602..ea40ba2bb 100644 --- a/include/haproxy/quic_tune-t.h +++ b/include/haproxy/quic_tune-t.h @@ -18,6 +18,8 @@ #define QUIC_DFLT_BE_MAX_IDLE_TIMEOUT 30000 /* milliseconds */ /* Default Retry threshold */ #define QUIC_DFLT_SEC_RETRY_THRESHOLD 100 /* in connection openings */ +/* Default Stateless Reset rate limit */ +#define QUIC_DFLT_SEC_STATELESS_RESET_RATE 1000 /* packets/s, all threads */ /* Default settings related to flow-control */ #define QUIC_DFLT_FE_STREAM_DATA_RATIO 90 #define QUIC_DFLT_BE_STREAM_DATA_RATIO 90 @@ -63,6 +65,7 @@ struct quic_tune { } be; uint64_t mem_tx_max; + uint sec_stateless_reset_rate; /* max total Stateless Reset pkts/s, all threads (0 = unlimited) */ }; #endif /* USE_QUIC */ diff --git a/src/cfgparse-quic.c b/src/cfgparse-quic.c index 7b9da28c7..5a005d206 100644 --- a/src/cfgparse-quic.c +++ b/src/cfgparse-quic.c @@ -48,6 +48,7 @@ struct quic_tune quic_tune = { .fb_opts = QUIC_TUNE_FB_TX_PACING|QUIC_TUNE_FB_TX_UDP_GSO, }, .mem_tx_max = QUIC_MAX_TX_MEM, + .sec_stateless_reset_rate = QUIC_DFLT_SEC_STATELESS_RESET_RATE, }; static int bind_parse_quic_force_retry(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) @@ -424,6 +425,9 @@ static int cfg_parse_quic_tune_setting(char **args, int section_type, else if (strcmp(suffix, "fe.sec.retry-threshold") == 0) { quic_tune.fe.sec_retry_threshold = arg; } + else if (strcmp(suffix, "sec.stateless-reset-rate") == 0) { + quic_tune.sec_stateless_reset_rate = arg; + } else if (strcmp(suffix, "be.stream.data-ratio") == 0 || strcmp(suffix, "fe.stream.data-ratio") == 0) { uint *ptr = (suffix[0] == 'b') ? &quic_tune.be.stream_data_ratio : @@ -532,6 +536,7 @@ static int cfg_parse_quic_tune_on_off(char **args, int section_type, struct prox static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.quic.listen", cfg_parse_quic_tune_on_off }, { CFG_GLOBAL, "tune.quic.mem.tx-max", cfg_parse_quic_tune_setting }, + { CFG_GLOBAL, "tune.quic.sec.stateless-reset-rate", cfg_parse_quic_tune_setting }, { CFG_GLOBAL, "tune.quic.zero-copy-fwd-send", cfg_parse_quic_tune_on_off }, { CFG_GLOBAL, "tune.quic.fe.cc.cubic-min-losses", cfg_parse_quic_tune_setting }, diff --git a/src/quic_tx.c b/src/quic_tx.c index da3cd4d44..00193fd9b 100644 --- a/src/quic_tx.c +++ b/src/quic_tx.c @@ -16,6 +16,9 @@ #include +#include +#include +#include #include #include #include @@ -1193,6 +1196,13 @@ int send_version_negotiation(int fd, struct sockaddr_storage *addr, return !ret; } +/* Stateless Reset rate limiting: each thread gets its own share of + * quic_tune.sec_stateless_reset_rate (the configured total, all + * threads), and its own freq_ctr to track how many it already sent. + */ +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 UDP socket to * Return 1 if succeeded, 0 if not. @@ -1208,6 +1218,12 @@ int send_stateless_reset(struct listener *l, struct sockaddr_storage *dstaddr, TRACE_ENTER(QUIC_EV_STATELESS_RST); + if (quic_stateless_reset_rate && + !freq_ctr_remain(&quic_stateless_reset_freq_ctr, quic_stateless_reset_rate, 0)) { + TRACE_DEVEL("stateless reset rate limited", QUIC_EV_STATELESS_RST); + goto leave; + } + /* RFC 9000 10.3. Stateless Reset * * Endpoints MUST discard packets that are too small to be valid QUIC @@ -1260,6 +1276,8 @@ int send_stateless_reset(struct listener *l, struct sockaddr_storage *dstaddr, ret = 1; HA_ATOMIC_INC(&prx_counters->stateless_reset_sent); + if (quic_stateless_reset_rate) + update_freq_ctr(&quic_stateless_reset_freq_ctr, 1); TRACE_PROTO("stateless reset sent", QUIC_EV_STATELESS_RST, NULL, &rxpkt->dcid); leave: TRACE_LEAVE(QUIC_EV_STATELESS_RST); @@ -2359,6 +2377,18 @@ int quic_tx_unittest(int argc, char **argv) } REGISTER_UNITTEST("quic_tx", quic_tx_unittest); +static int quic_stateless_reset_rate_init(void) +{ + /* split N across threads so the totals add up to exactly N: this + * thread gets (N + tid) / nbthread. No flat ceil() here, since that + * could push the total above N. + */ + quic_stateless_reset_rate = + (quic_tune.sec_stateless_reset_rate + tid) / global.nbthread; + return 1; +} +REGISTER_PER_THREAD_INIT(quic_stateless_reset_rate_init); + /* * Local variables: * c-indent-level: 8