This is an automated email from the ASF dual-hosted git repository. bcall pushed a commit to branch 9.2.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/9.2.x by this push: new b8c6a23b74 proxy.config.http2.max_continuation_frames_per_minute (#11206) b8c6a23b74 is described below commit b8c6a23b74af1772e5cb0de25b38c234a418cb1d Author: Masakazu Kitajo <mas...@apache.org> AuthorDate: Wed Apr 3 09:31:37 2024 -0600 proxy.config.http2.max_continuation_frames_per_minute (#11206) This adds the ability to rate limite HTTP/2 CONTINUATION frames per stream per minute. Co-authored-by: Brian Neradt <brian.ner...@gmail.com> --- doc/admin-guide/files/records.config.en.rst | 11 +++- .../statistics/core/http-connection.en.rst | 11 +++- iocore/net/P_SNIActionPerformer.h | 17 ++++++ iocore/net/SSLSNIConfig.cc | 4 ++ iocore/net/TLSSNISupport.h | 1 + iocore/net/YamlSNIConfig.cc | 4 ++ iocore/net/YamlSNIConfig.h | 2 + mgmt/RecordsConfig.cc | 2 + proxy/http2/HTTP2.cc | 66 ++++++++++++---------- proxy/http2/HTTP2.h | 2 + proxy/http2/Http2ConnectionState.cc | 36 ++++++++++-- proxy/http2/Http2ConnectionState.h | 12 ++-- 12 files changed, 126 insertions(+), 42 deletions(-) diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst index f3df888708..979c8bda2f 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -4287,8 +4287,15 @@ HTTP/2 Configuration .. ts:cv:: CONFIG proxy.config.http2.max_rst_stream_frames_per_minute INT 200 :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 + Specifies how many RST_STREAM frames |TS| receives per minute at maximum. + Clients exceeding this limit will be immediately disconnected with an error + code of ENHANCE_YOUR_CALM. + +.. ts:cv:: CONFIG proxy.config.http2.max_continuation_frames_per_minute INT 120 + :reloadable: + + Specifies how many CONTINUATION frames |TS| receives per minute at maximum. + Clients exceeding 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 diff --git a/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst b/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst index b22da8e1c6..ee47a147c0 100644 --- a/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst +++ b/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst @@ -263,10 +263,17 @@ HTTP/2 .. 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 + Represents the total number of HTTP/2 connections closed 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.max_continuation_frames_per_minute_exceeded integer + :type: counter + + Represents the total number of HTTP/2 connections closed for exceeding the + maximum allowed number of ``CONTINUATION`` frames per minute limit which is + configured by :ts:cv:`proxy.config.http2.max_continuation_frames_per_minute`. + .. ts:stat:: global proxy.process.http2.insufficient_avg_window_update integer :type: counter diff --git a/iocore/net/P_SNIActionPerformer.h b/iocore/net/P_SNIActionPerformer.h index e223ac7d0b..eebe44b75a 100644 --- a/iocore/net/P_SNIActionPerformer.h +++ b/iocore/net/P_SNIActionPerformer.h @@ -186,6 +186,23 @@ private: int value = -1; }; +class HTTP2MaxContinuationFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxContinuationFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxContinuationFramesPerMinute() override {} + + int + SNIAction(TLSSNISupport *snis, const Context &ctx) const override + { + snis->hints_from_sni.http2_max_continuation_frames_per_minute = value; + return SSL_TLSEXT_ERR_OK; + } + +private: + int value = -1; +}; + class TunnelDestination : public ActionItem { public: diff --git a/iocore/net/SSLSNIConfig.cc b/iocore/net/SSLSNIConfig.cc index a7071013f6..942e6c420f 100644 --- a/iocore/net/SSLSNIConfig.cc +++ b/iocore/net/SSLSNIConfig.cc @@ -151,6 +151,10 @@ SNIConfigParams::load_sni_config() ai->actions.push_back( std::make_unique<HTTP2MaxRstStreamFramesPerMinute>(item.http2_max_rst_stream_frames_per_minute.value())); } + if (item.http2_max_continuation_frames_per_minute.has_value()) { + ai->actions.push_back( + std::make_unique<HTTP2MaxContinuationFramesPerMinute>(item.http2_max_continuation_frames_per_minute.value())); + } ai->actions.push_back(std::make_unique<SNI_IpAllow>(item.ip_allow, item.fqdn)); diff --git a/iocore/net/TLSSNISupport.h b/iocore/net/TLSSNISupport.h index ba2d13e930..e8614ffa9b 100644 --- a/iocore/net/TLSSNISupport.h +++ b/iocore/net/TLSSNISupport.h @@ -56,6 +56,7 @@ public: std::optional<uint32_t> http2_max_ping_frames_per_minute; std::optional<uint32_t> http2_max_priority_frames_per_minute; std::optional<uint32_t> http2_max_rst_stream_frames_per_minute; + std::optional<uint32_t> http2_max_continuation_frames_per_minute; } hints_from_sni; protected: diff --git a/iocore/net/YamlSNIConfig.cc b/iocore/net/YamlSNIConfig.cc index 9a777b806f..7286197c9c 100644 --- a/iocore/net/YamlSNIConfig.cc +++ b/iocore/net/YamlSNIConfig.cc @@ -148,6 +148,7 @@ std::set<std::string> valid_sni_config_keys = {TS_fqdn, TS_http2_max_ping_frames_per_minute, TS_http2_max_priority_frames_per_minute, TS_http2_max_rst_stream_frames_per_minute, + TS_http2_max_continuation_frames_per_minute, TS_ip_allow, #if TS_USE_HELLO_CB || defined(OPENSSL_IS_BORINGSSL) TS_valid_tls_versions_in, @@ -193,6 +194,9 @@ template <> struct convert<YamlSNIConfig::Item> { if (node[TS_http2_max_rst_stream_frames_per_minute]) { item.http2_max_rst_stream_frames_per_minute = node[TS_http2_max_rst_stream_frames_per_minute].as<int>(); } + if (node[TS_http2_max_continuation_frames_per_minute]) { + item.http2_max_continuation_frames_per_minute = node[TS_http2_max_continuation_frames_per_minute].as<int>(); + } // enum if (node[TS_verify_client]) { diff --git a/iocore/net/YamlSNIConfig.h b/iocore/net/YamlSNIConfig.h index b297bd5c16..8165dc336c 100644 --- a/iocore/net/YamlSNIConfig.h +++ b/iocore/net/YamlSNIConfig.h @@ -60,6 +60,7 @@ TSDECL(http2_max_settings_frames_per_minute); TSDECL(http2_max_ping_frames_per_minute); TSDECL(http2_max_priority_frames_per_minute); TSDECL(http2_max_rst_stream_frames_per_minute); +TSDECL(http2_max_continuation_frames_per_minute); TSDECL(host_sni_policy); #undef TSDECL @@ -94,6 +95,7 @@ struct YamlSNIConfig { std::optional<int> http2_max_ping_frames_per_minute; std::optional<int> http2_max_priority_frames_per_minute; std::optional<int> http2_max_rst_stream_frames_per_minute; + std::optional<int> http2_max_continuation_frames_per_minute; bool tunnel_prewarm_srv = false; uint32_t tunnel_prewarm_min = 0; diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc index b63e0523c2..a3752ea835 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -1395,6 +1395,8 @@ static const RecordElement RecordsConfig[] = , {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.max_continuation_frames_per_minute", RECD_INT, "120", 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} diff --git a/proxy/http2/HTTP2.cc b/proxy/http2/HTTP2.cc index 04813d2212..a3a5a0ac78 100644 --- a/proxy/http2/HTTP2.cc +++ b/proxy/http2/HTTP2.cc @@ -85,6 +85,8 @@ 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_MAX_CONTINUATION_FRAMES_PER_MINUTE_EXCEEDED_NAME = + "proxy.process.http2.max_continuation_frames_per_minute_exceeded"; static const char *const HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE_NAME = "proxy.process.http2.insufficient_avg_window_update"; static const char *const HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_IN_NAME = "proxy.process.http2.max_concurrent_streams_exceeded_in"; @@ -798,36 +800,37 @@ 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::stream_error_sampling_threshold = 10; -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; -uint32_t Http2::write_buffer_block_size = 262144; -float Http2::write_size_threshold = 0.5; -uint32_t Http2::write_time_threshold = 100; -uint32_t Http2::buffer_water_mark = 0; +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::stream_error_sampling_threshold = 10; +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; +uint32_t Http2::max_continuation_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::write_buffer_block_size = 262144; +float Http2::write_size_threshold = 0.5; +uint32_t Http2::write_time_threshold = 100; +uint32_t Http2::buffer_water_mark = 0; void Http2::init() @@ -853,6 +856,7 @@ Http2::init() 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_EstablishStaticConfigInt32U(max_continuation_frames_per_minute, "proxy.config.http2.max_continuation_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"); @@ -923,6 +927,8 @@ Http2::init() 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_MAX_CONTINUATION_FRAMES_PER_MINUTE_EXCEEDED_NAME, RECD_INT, + RECP_PERSISTENT, static_cast<int>(HTTP2_STAT_MAX_CONTINUATION_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); RecRegisterRawStat(http2_rsb, RECT_PROCESS, HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_IN_NAME, RECD_INT, RECP_PERSISTENT, diff --git a/proxy/http2/HTTP2.h b/proxy/http2/HTTP2.h index 5847865a9a..857b199c05 100644 --- a/proxy/http2/HTTP2.h +++ b/proxy/http2/HTTP2.h @@ -105,6 +105,7 @@ enum { 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_MAX_CONTINUATION_FRAMES_PER_MINUTE_EXCEEDED, HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE, HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_IN, HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_OUT, @@ -404,6 +405,7 @@ public: 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 uint32_t max_continuation_frames_per_minute; static float min_avg_window_update; static uint32_t con_slow_log_threshold; static uint32_t stream_slow_log_threshold; diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc index b36e5c1179..b089048eb1 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -924,6 +924,18 @@ rcv_continuation_frame(Http2ConnectionState &cstate, const Http2Frame &frame) } } + // Update CONTINUATION frame count per minute. + cstate.increment_received_continuation_frame_count(); + // Close this connection if its CONTINUATION frame count exceeds a limit. + if (cstate.configured_max_continuation_frames_per_minute != 0 && + cstate.get_received_continuation_frame_count() > cstate.configured_max_continuation_frames_per_minute) { + HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_CONTINUATION_FRAMES_PER_MINUTE_EXCEEDED, this_ethread()); + Http2StreamDebug(cstate.session, stream_id, "Observed too frequent CONTINUATION frames: %u frames within a last minute", + cstate.get_received_continuation_frame_count()); + return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM, + "reset too frequent CONTINUATION frames"); + } + uint32_t header_blocks_offset = stream->header_blocks_length; stream->header_blocks_length += payload_length; @@ -1088,10 +1100,11 @@ Http2ConnectionState::init(Http2CommonSession *ssn) dependency_tree = new DependencyTree(Http2::max_concurrent_streams_in); } - configured_max_settings_frames_per_minute = Http2::max_settings_frames_per_minute; - configured_max_ping_frames_per_minute = Http2::max_ping_frames_per_minute; - configured_max_priority_frames_per_minute = Http2::max_priority_frames_per_minute; - configured_max_rst_stream_frames_per_minute = Http2::max_rst_stream_frames_per_minute; + configured_max_settings_frames_per_minute = Http2::max_settings_frames_per_minute; + configured_max_ping_frames_per_minute = Http2::max_ping_frames_per_minute; + configured_max_priority_frames_per_minute = Http2::max_priority_frames_per_minute; + configured_max_rst_stream_frames_per_minute = Http2::max_rst_stream_frames_per_minute; + configured_max_continuation_frames_per_minute = Http2::max_continuation_frames_per_minute; if (auto snis = dynamic_cast<TLSSNISupport *>(session->get_netvc()); snis) { if (snis->hints_from_sni.http2_max_settings_frames_per_minute.has_value()) { configured_max_settings_frames_per_minute = snis->hints_from_sni.http2_max_settings_frames_per_minute.value(); @@ -1105,6 +1118,9 @@ Http2ConnectionState::init(Http2CommonSession *ssn) if (snis->hints_from_sni.http2_max_rst_stream_frames_per_minute.has_value()) { configured_max_rst_stream_frames_per_minute = snis->hints_from_sni.http2_max_rst_stream_frames_per_minute.value(); } + if (snis->hints_from_sni.http2_max_continuation_frames_per_minute.has_value()) { + configured_max_continuation_frames_per_minute = snis->hints_from_sni.http2_max_continuation_frames_per_minute.value(); + } } _cop = ActivityCop<Http2Stream>(this->mutex, &stream_list, 1); @@ -2140,6 +2156,18 @@ Http2ConnectionState::get_received_rst_stream_frame_count() return this->_received_rst_stream_frame_counter.get_count(); } +void +Http2ConnectionState::increment_received_continuation_frame_count() +{ + this->_received_continuation_frame_counter.increment(); +} + +uint32_t +Http2ConnectionState::get_received_continuation_frame_count() +{ + return this->_received_continuation_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 diff --git a/proxy/http2/Http2ConnectionState.h b/proxy/http2/Http2ConnectionState.h index 76d2e2a8e1..fff7763f2a 100644 --- a/proxy/http2/Http2ConnectionState.h +++ b/proxy/http2/Http2ConnectionState.h @@ -102,10 +102,11 @@ public: Http2ConnectionSettings server_settings; Http2ConnectionSettings client_settings; - uint32_t configured_max_settings_frames_per_minute = 0; - uint32_t configured_max_ping_frames_per_minute = 0; - uint32_t configured_max_priority_frames_per_minute = 0; - uint32_t configured_max_rst_stream_frames_per_minute = 0; + uint32_t configured_max_settings_frames_per_minute = 0; + uint32_t configured_max_ping_frames_per_minute = 0; + uint32_t configured_max_priority_frames_per_minute = 0; + uint32_t configured_max_rst_stream_frames_per_minute = 0; + uint32_t configured_max_continuation_frames_per_minute = 0; void init(Http2CommonSession *ssn); void send_connection_preface(); @@ -174,6 +175,8 @@ public: uint32_t get_received_priority_frame_count(); void increment_received_rst_stream_frame_count(); uint32_t get_received_rst_stream_frame_count(); + void increment_received_continuation_frame_count(); + uint32_t get_received_continuation_frame_count(); ssize_t client_rwnd() const; Http2ErrorCode increment_client_rwnd(size_t amount); @@ -220,6 +223,7 @@ private: Http2FrequencyCounter _received_ping_frame_counter; Http2FrequencyCounter _received_priority_frame_counter; Http2FrequencyCounter _received_rst_stream_frame_counter; + Http2FrequencyCounter _received_continuation_frame_counter; // NOTE: Id of stream which MUST receive CONTINUATION frame. // - [RFC 7540] 6.2 HEADERS