Add an HTTP/2 related rate limiting (#10565)
authorMasakazu Kitajo <maskit@apache.org>
Mon, 9 Oct 2023 16:02:37 +0000 (01:02 +0900)
committerAdrian Bunk <bunk@debian.org>
Sun, 28 Apr 2024 18:24:00 +0000 (21:24 +0300)
Gbp-Pq: Name 0002-Add-an-HTTP-2-related-rate-limiting-10565.patch

doc/admin-guide/files/records.config.en.rst
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst
mgmt/RecordsConfig.cc
proxy/http2/HTTP2.cc
proxy/http2/HTTP2.h
proxy/http2/Http2ConnectionState.cc
proxy/http2/Http2ConnectionState.h

index 38c9ee4794a232b51acc2a332f25087654ab43b2..d2d390f5a863cd90fc71ce5472ac1a4bdac4515b 100644 (file)
@@ -3686,6 +3686,13 @@ HTTP/2 Configuration
    This limit only will be enforced if :ts:cv:`proxy.config.http2.stream_priority_enabled`
    is set to 1.
 
+.. ts:cv:: CONFIG proxy.config.http2.max_rst_stream_frames_per_minute INT 14
+   :reloadable:
+
+   Specifies how many RST_STREAM frames |TS| receives for a minute at maximum.
+   Clients exceeded this limit will be immediately disconnected with an error
+   code of ENHANCE_YOUR_CALM.
+
 .. ts:cv:: CONFIG proxy.config.http2.min_avg_window_update FLOAT 2560.0
    :reloadable:
 
index d2e9014ffde7e1cd0df9af058f5105ffc25f6c40..b14e72bd7558a83738d6d53ca603536f7409c875 100644 (file)
@@ -238,6 +238,13 @@ HTTP/2
    maximum allowed number of priority frames per minute limit which is configured by
    :ts:cv:`proxy.config.http2.max_priority_frames_per_minute`.
 
+.. ts:stat:: global proxy.process.http2.max_rst_stream_frames_per_minute_exceeded integer
+   :type: counter
+
+   Represents the total number of closed HTTP/2 connections for exceeding the
+   maximum allowed number of rst_stream frames per minute limit which is configured by
+   :ts:cv:`proxy.config.http2.max_rst_stream_frames_per_minute`.
+
 .. ts:stat:: global proxy.process.http2.insufficient_avg_window_update integer
    :type: counter
 
index 79b025b64fb3cfb769a82e666d3d31f0315919a0..c2d84dc0e87b447fd2307c7f3d7c959f3d3f14cd 100644 (file)
@@ -1346,6 +1346,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http2.max_priority_frames_per_minute", RECD_INT, "120", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.http2.max_rst_stream_frames_per_minute", RECD_INT, "200", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.http2.min_avg_window_update", RECD_FLOAT, "2560.0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.http2.header_table_size_limit", RECD_INT, "65536", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
index f928ea33a5a15ab702e2d4b814354751f08f3d30..cc07c9a018897b9e5224858f558ad5607985a1cb 100644 (file)
@@ -71,6 +71,8 @@ static const char *const HTTP2_STAT_MAX_PING_FRAMES_PER_MINUTE_EXCEEDED_NAME =
   "proxy.process.http2.max_ping_frames_per_minute_exceeded";
 static const char *const HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED_NAME =
   "proxy.process.http2.max_priority_frames_per_minute_exceeded";
+static const char *const HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED_NAME =
+  "proxy.process.http2.max_rst_stream_frames_per_minute_exceeded";
 static const char *const HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE_NAME = "proxy.process.http2.insufficient_avg_window_update";
 
 union byte_pointer {
@@ -726,30 +728,31 @@ http2_decode_header_blocks(HTTPHdr *hdr, const uint8_t *buf_start, const uint32_
 }
 
 // Initialize this subsystem with librecords configs (for now)
-uint32_t Http2::max_concurrent_streams_in      = 100;
-uint32_t Http2::min_concurrent_streams_in      = 10;
-uint32_t Http2::max_active_streams_in          = 0;
-bool Http2::throttling                         = false;
-uint32_t Http2::stream_priority_enabled        = 0;
-uint32_t Http2::initial_window_size            = 65535;
-uint32_t Http2::max_frame_size                 = 16384;
-uint32_t Http2::header_table_size              = 4096;
-uint32_t Http2::max_header_list_size           = 4294967295;
-uint32_t Http2::accept_no_activity_timeout     = 120;
-uint32_t Http2::no_activity_timeout_in         = 120;
-uint32_t Http2::active_timeout_in              = 0;
-uint32_t Http2::push_diary_size                = 256;
-uint32_t Http2::zombie_timeout_in              = 0;
-float Http2::stream_error_rate_threshold       = 0.1;
-uint32_t Http2::max_settings_per_frame         = 7;
-uint32_t Http2::max_settings_per_minute        = 14;
-uint32_t Http2::max_settings_frames_per_minute = 14;
-uint32_t Http2::max_ping_frames_per_minute     = 60;
-uint32_t Http2::max_priority_frames_per_minute = 120;
-float Http2::min_avg_window_update             = 2560.0;
-uint32_t Http2::con_slow_log_threshold         = 0;
-uint32_t Http2::stream_slow_log_threshold      = 0;
-uint32_t Http2::header_table_size_limit        = 65536;
+uint32_t Http2::max_concurrent_streams_in        = 100;
+uint32_t Http2::min_concurrent_streams_in        = 10;
+uint32_t Http2::max_active_streams_in            = 0;
+bool Http2::throttling                           = false;
+uint32_t Http2::stream_priority_enabled          = 0;
+uint32_t Http2::initial_window_size              = 65535;
+uint32_t Http2::max_frame_size                   = 16384;
+uint32_t Http2::header_table_size                = 4096;
+uint32_t Http2::max_header_list_size             = 4294967295;
+uint32_t Http2::accept_no_activity_timeout       = 120;
+uint32_t Http2::no_activity_timeout_in           = 120;
+uint32_t Http2::active_timeout_in                = 0;
+uint32_t Http2::push_diary_size                  = 256;
+uint32_t Http2::zombie_timeout_in                = 0;
+float Http2::stream_error_rate_threshold         = 0.1;
+uint32_t Http2::max_settings_per_frame           = 7;
+uint32_t Http2::max_settings_per_minute          = 14;
+uint32_t Http2::max_settings_frames_per_minute   = 14;
+uint32_t Http2::max_ping_frames_per_minute       = 60;
+uint32_t Http2::max_priority_frames_per_minute   = 120;
+uint32_t Http2::max_rst_stream_frames_per_minute = 200;
+float Http2::min_avg_window_update               = 2560.0;
+uint32_t Http2::con_slow_log_threshold           = 0;
+uint32_t Http2::stream_slow_log_threshold        = 0;
+uint32_t Http2::header_table_size_limit          = 65536;
 
 void
 Http2::init()
@@ -773,6 +776,7 @@ Http2::init()
   REC_EstablishStaticConfigInt32U(max_settings_frames_per_minute, "proxy.config.http2.max_settings_frames_per_minute");
   REC_EstablishStaticConfigInt32U(max_ping_frames_per_minute, "proxy.config.http2.max_ping_frames_per_minute");
   REC_EstablishStaticConfigInt32U(max_priority_frames_per_minute, "proxy.config.http2.max_priority_frames_per_minute");
+  REC_EstablishStaticConfigInt32U(max_rst_stream_frames_per_minute, "proxy.config.http2.max_rst_stream_frames_per_minute");
   REC_EstablishStaticConfigFloat(min_avg_window_update, "proxy.config.http2.min_avg_window_update");
   REC_EstablishStaticConfigInt32U(con_slow_log_threshold, "proxy.config.http2.connection.slow.log.threshold");
   REC_EstablishStaticConfigInt32U(stream_slow_log_threshold, "proxy.config.http2.stream.slow.log.threshold");
@@ -839,6 +843,8 @@ Http2::init()
                      static_cast<int>(HTTP2_STAT_MAX_PING_FRAMES_PER_MINUTE_EXCEEDED), RecRawStatSyncSum);
   RecRegisterRawStat(http2_rsb, RECT_PROCESS, HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED_NAME, RECD_INT, RECP_PERSISTENT,
                      static_cast<int>(HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED), RecRawStatSyncSum);
+  RecRegisterRawStat(http2_rsb, RECT_PROCESS, HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED_NAME, RECD_INT, RECP_PERSISTENT,
+                     static_cast<int>(HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED), RecRawStatSyncSum);
   RecRegisterRawStat(http2_rsb, RECT_PROCESS, HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE_NAME, RECD_INT, RECP_PERSISTENT,
                      static_cast<int>(HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE), RecRawStatSyncSum);
 }
index 6b193e7dae60faa1905a7ff24f045c85c6a48c52..7fe657e5d018f8be1127bb0882c2b3adf559ee12 100644 (file)
@@ -92,6 +92,7 @@ enum {
   HTTP2_STAT_MAX_SETTINGS_FRAMES_PER_MINUTE_EXCEEDED,
   HTTP2_STAT_MAX_PING_FRAMES_PER_MINUTE_EXCEEDED,
   HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED,
+  HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED,
   HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE,
 
   HTTP2_N_STATS // Terminal counter, NOT A STAT INDEX.
@@ -388,6 +389,7 @@ public:
   static uint32_t max_settings_frames_per_minute;
   static uint32_t max_ping_frames_per_minute;
   static uint32_t max_priority_frames_per_minute;
+  static uint32_t max_rst_stream_frames_per_minute;
   static float min_avg_window_update;
   static uint32_t con_slow_log_threshold;
   static uint32_t stream_slow_log_threshold;
index c2a7cfbcddd3e27a8554a2f99d7897eea2c13c2c..ce66c31eb79a05681b3527d1d1a5fb0340a5c0a1 100644 (file)
@@ -522,6 +522,17 @@ rcv_rst_stream_frame(Http2ConnectionState &cstate, const Http2Frame &frame)
                       "reset frame wrong length");
   }
 
+  // Update RST_STREAM frame count per minute
+  cstate.increment_received_rst_stream_frame_count();
+  // Close this connection if its RST_STREAM frame count exceeds a limit
+  if (cstate.get_received_rst_stream_frame_count() > Http2::max_rst_stream_frames_per_minute) {
+    HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED, this_ethread());
+    Http2StreamDebug(cstate.ua_session, stream_id, "Observed too frequent RST_STREAM frames: %u frames within a last minute",
+                     cstate.get_received_settings_frame_count());
+    return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
+                      "reset too frequent RST_STREAM frames");
+  }
+
   if (stream == nullptr || !stream->change_state(frame.header().type, frame.header().flags)) {
     // If a RST_STREAM frame identifying an idle stream is received, the
     // recipient MUST treat this as a connection error of type PROTOCOL_ERROR.
@@ -1953,6 +1964,18 @@ Http2ConnectionState::get_received_priority_frame_count()
   return this->_received_priority_frame_counter.get_count();
 }
 
+void
+Http2ConnectionState::increment_received_rst_stream_frame_count()
+{
+  this->_received_rst_stream_frame_counter.increment();
+}
+
+uint32_t
+Http2ConnectionState::get_received_rst_stream_frame_count()
+{
+  return this->_received_rst_stream_frame_counter.get_count();
+}
+
 // Return min_concurrent_streams_in when current client streams number is larger than max_active_streams_in.
 // Main purpose of this is preventing DDoS Attacks.
 unsigned
index a30990ee2b8d6c78972bb5bf4b63ad3b3292bcb6..ed4f5cd47fbca18c68365c33199ea385b8aab9c3 100644 (file)
@@ -323,6 +323,8 @@ public:
   uint32_t get_received_ping_frame_count();
   void increment_received_priority_frame_count();
   uint32_t get_received_priority_frame_count();
+  void increment_received_rst_stream_frame_count();
+  uint32_t get_received_rst_stream_frame_count();
 
   ssize_t client_rwnd() const;
   Http2ErrorCode increment_client_rwnd(size_t amount);
@@ -368,6 +370,7 @@ private:
   Http2FrequencyCounter _received_settings_frame_counter;
   Http2FrequencyCounter _received_ping_frame_counter;
   Http2FrequencyCounter _received_priority_frame_counter;
+  Http2FrequencyCounter _received_rst_stream_frame_counter;
 
   // NOTE: Id of stream which MUST receive CONTINUATION frame.
   //   - [RFC 7540] 6.2 HEADERS