]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: quic: add tune.quic.sec.stateless-reset-rate
authorFrederic Lecaille <flecaille@haproxy.com>
Thu, 20 Aug 2026 16:23:18 +0000 (18:23 +0200)
committerFrederic Lecaille <flecaille@haproxy.com>
Mon, 24 Aug 2026 17:16:05 +0000 (19:16 +0200)
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 <tid> 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.

doc/configuration.txt
include/haproxy/quic_tune-t.h
src/cfgparse-quic.c
src/quic_tx.c

index cfe1f397a820de3daed0532818cb822636ac7b0c..89a08851166098b71ab0cee0ac56ed48f247093f 100644 (file)
@@ -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 <size>
   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 <number>
+  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
+  <number> 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.
index 56caab602d2da72c86f40533c45cc10f20505ead..ea40ba2bb8cdc83aeaa689e36667595fd96bc4de 100644 (file)
@@ -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 */
index 7b9da28c7365fc9cdf1771da49cda0b83e59a923..5a005d206e4dbbe08fc7b9249dd2c0467c07441a 100644 (file)
@@ -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 },
index da3cd4d4411c8b5fde9a1a06b6c2c0c7d1a894d0..00193fd9b058a9333404ff2fa5b886b257d65786 100644 (file)
@@ -16,6 +16,9 @@
 
 #include <errno.h>
 
+#include <haproxy/freq_ctr.h>
+#include <haproxy/global.h>
+#include <haproxy/init.h>
 #include <haproxy/pool.h>
 #include <haproxy/trace.h>
 #include <haproxy/quic_cc_drs.h>
@@ -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 <pkt> RX packet information
  * from <fd> UDP socket to <dst>
  * 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