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 b28ad74f11 Add an HTTP/2 related rate limiting (#10564) b28ad74f11 is described below commit b28ad74f117307e8de206f1de70c3fa716f90682 Author: Masakazu Kitajo <mas...@apache.org> AuthorDate: Tue Oct 10 00:02:18 2023 +0900 Add an HTTP/2 related rate limiting (#10564) --- doc/admin-guide/files/records.config.en.rst | 7 + doc/admin-guide/files/sni.yaml.en.rst | 190 +++++++++++---------- .../statistics/core/http-connection.en.rst | 7 + iocore/net/P_SNIActionPerformer.h | 68 ++++++++ iocore/net/SSLSNIConfig.cc | 13 ++ iocore/net/TLSSNISupport.h | 4 + iocore/net/YamlSNIConfig.cc | 16 ++ iocore/net/YamlSNIConfig.h | 8 + mgmt/RecordsConfig.cc | 2 + proxy/http2/HTTP2.cc | 64 +++---- proxy/http2/HTTP2.h | 2 + proxy/http2/Http2ConnectionState.cc | 54 +++++- proxy/http2/Http2ConnectionState.h | 8 + 13 files changed, 323 insertions(+), 120 deletions(-) diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst index 50d0dfd941..e6865b8c80 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -4272,6 +4272,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: diff --git a/doc/admin-guide/files/sni.yaml.en.rst b/doc/admin-guide/files/sni.yaml.en.rst index 5c11420142..8dc078eacb 100644 --- a/doc/admin-guide/files/sni.yaml.en.rst +++ b/doc/admin-guide/files/sni.yaml.en.rst @@ -52,122 +52,138 @@ for a more detailed description of HTTP/2 connection coalescing. .. _override-host-sni-policy: .. _override-h2-properties: -========================= ========= ======================================================================================== -Key Direction Meaning -========================= ========= ======================================================================================== -fqdn Both Fully Qualified Domain Name. This item is used if the SNI value matches this. +====================================== ========= ======================================================================================== +Key Direction Meaning +====================================== ========= ======================================================================================== +fqdn Both Fully Qualified Domain Name. This item is used if the SNI value matches this. + +ip_allow Inbound Specify a list of client IP address, subnets, or ranges what are allowed to complete + the connection. This list is comma separated. IPv4 and IPv6 addresses can be specified. + Here is an example list: 192.168.1.0/24,192.168.10.1-4. This would allow connections + from clients in the 19.168.1.0 network or in the range from 192.168.10.1 to 192.168.1.4. + +verify_server_policy Outbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. + + By default this is :ts:cv:`proxy.config.ssl.client.verify.server.policy`. + This controls how |TS| evaluated the origin certificate. -ip_allow Inbound Specify a list of client IP address, subnets, or ranges what are allowed to complete - the connection. This list is comma separated. IPv4 and IPv6 addresses can be specified. - Here is an example list: 192.168.1.0/24,192.168.10.1-4. This would allow connections - from clients in the 19.168.1.0 network or in the range from 192.168.10.1 to 192.168.1.4. +verify_server_properties Outbound One of the values :code:`NONE`, :code:`SIGNATURE`, :code:`NAME`, and :code:`ALL` -verify_server_policy Outbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. + By default this is :ts:cv:`proxy.config.ssl.client.verify.server.properties`. + This controls what |TS| checks when evaluating the origin certificate. - By default this is :ts:cv:`proxy.config.ssl.client.verify.server.policy`. - This controls how |TS| evaluated the origin certificate. +verify_client Outbound One of the values :code:`NONE`, :code:`MODERATE`, or :code:`STRICT`. + If ``NONE`` is specified, |TS| requests no certificate. If ``MODERATE`` is specified + |TS| will verify a certificate that is presented by the client, but it will not + fail the TLS handshake if no certificate is presented. If ``STRICT`` is specified + the client must present a certificate during the TLS handshake. -verify_server_properties Outbound One of the values :code:`NONE`, :code:`SIGNATURE`, :code:`NAME`, and :code:`ALL` + By default this is :ts:cv:`proxy.config.ssl.client.certification_level`. - By default this is :ts:cv:`proxy.config.ssl.client.verify.server.properties`. - This controls what |TS| checks when evaluating the origin certificate. +verify_client_ca_certs Both Specifies an alternate set of certificate authority certs to use to verify the + client cert. The value must be either a file path, or a nested set of key / + value pairs. If the value is a file path, it must specify a file containing the + CA certs. Otherwise, there should be up to two nested pairs. The possible keys + are ``file`` and ``dir``. The value for ``file`` must be a file path for a file + containing CA certs. The value for ``dir`` must be a file path for an OpenSSL + X509 hashed directory containing CA certs. If a given file path does not being + with ``/`` , it must be relative to the |TS| configuration directory. + ``verify_client_ca_certs`` can only be used with capbilities provided by + OpenSSL 1.0.2 or later. -verify_client Outbound One of the values :code:`NONE`, :code:`MODERATE`, or :code:`STRICT`. - If ``NONE`` is specified, |TS| requests no certificate. If ``MODERATE`` is specified - |TS| will verify a certificate that is presented by the client, but it will not - fail the TLS handshake if no certificate is presented. If ``STRICT`` is specified - the client must present a certificate during the TLS handshake. +host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. - By default this is :ts:cv:`proxy.config.ssl.client.certification_level`. + If not specified, the value of :ts:cv:`proxy.config.http.host_sni_policy` is used. + This controls how policy impacting mismatches between host header and SNI values are + dealt with. For details about hos this configuration behaves, see the corresponding + :ts:cv:`proxy.config.http.host_sni_policy` :file:`records.config` documentation. -verify_client_ca_certs Both Specifies an alternate set of certificate authority certs to use to verify the - client cert. The value must be either a file path, or a nested set of key / - value pairs. If the value is a file path, it must specify a file containing the - CA certs. Otherwise, there should be up to two nested pairs. The possible keys - are ``file`` and ``dir``. The value for ``file`` must be a file path for a file - containing CA certs. The value for ``dir`` must be a file path for an OpenSSL - X509 hashed directory containing CA certs. If a given file path does not being - with ``/`` , it must be relative to the |TS| configuration directory. - ``verify_client_ca_certs`` can only be used with capbilities provided by - OpenSSL 1.0.2 or later. + Note that this particular configuration will be inspected at the time the HTTP Host + header field is processed. Further, this policy check will be keyed off of the Host header + field value rather than the SNI in this :file:`sni.yaml` file. This is done because + the Host header field is ultimately the resource that will be retrieved from the + origin and the administrator will intend to guard this resource rather than the SNI, + which a malicious user may alter to some other server value whose policies are more + lenient than the host he is trying to access. -host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. +valid_tls_versions_in Inbound This specifies the list of TLS protocols that will be offered to user agents during + the TLS negotiation. This replaces the global settings in + :ts:cv:`proxy.config.ssl.TLSv1`, :ts:cv:`proxy.config.ssl.TLSv1_1`, + :ts:cv:`proxy.config.ssl.TLSv1_2`, and :ts:cv:`proxy.config.ssl.TLSv1_3`. The potential + values are TLSv1, TLSv1_1, TLSv1_2, and TLSv1_3. You must list all protocols that |TS| + should offer to the client when using this key. This key is only valid for OpenSSL + 1.1.0 and later and BoringSSL. Older versions of OpenSSL do not provide a hook early enough to update + the SSL object. It is a syntax error for |TS| built against earlier versions. - If not specified, the value of :ts:cv:`proxy.config.http.host_sni_policy` is used. - This controls how policy impacting mismatches between host header and SNI values are - dealt with. For details about hos this configuration behaves, see the corresponding - :ts:cv:`proxy.config.http.host_sni_policy` :file:`records.config` documentation. +client_cert Outbound The file containing the client certificate to use for the outbound connection. - Note that this particular configuration will be inspected at the time the HTTP Host - header field is processed. Further, this policy check will be keyed off of the Host header - field value rather than the SNI in this :file:`sni.yaml` file. This is done because - the Host header field is ultimately the resource that will be retrieved from the - origin and the administrator will intend to guard this resource rather than the SNI, - which a malicious user may alter to some other server value whose policies are more - lenient than the host he is trying to access. + If this is relative, it is relative to the path in + :ts:cv:`proxy.config.ssl.client.cert.path`. If not set + :ts:cv:`proxy.config.ssl.client.cert.filename` is used. -valid_tls_versions_in Inbound This specifies the list of TLS protocols that will be offered to user agents during - the TLS negotiation. This replaces the global settings in - :ts:cv:`proxy.config.ssl.TLSv1`, :ts:cv:`proxy.config.ssl.TLSv1_1`, - :ts:cv:`proxy.config.ssl.TLSv1_2`, and :ts:cv:`proxy.config.ssl.TLSv1_3`. The potential - values are TLSv1, TLSv1_1, TLSv1_2, and TLSv1_3. You must list all protocols that |TS| - should offer to the client when using this key. This key is only valid for OpenSSL - 1.1.0 and later and BoringSSL. Older versions of OpenSSL do not provide a hook early enough to update - the SSL object. It is a syntax error for |TS| built against earlier versions. +client_key Outbound The file containing the client private key that corresponds to the certificate + for the outbound connection. -client_cert Outbound The file containing the client certificate to use for the outbound connection. + If this is relative, it is relative to the path in + :ts:cv:`proxy.config.ssl.client.private_key.path`. If not set, + |TS| tries to use a private key in client_cert. Otherwise, + :ts:cv:`proxy.config.ssl.client.private_key.filename` is used. - If this is relative, it is relative to the path in - :ts:cv:`proxy.config.ssl.client.cert.path`. If not set - :ts:cv:`proxy.config.ssl.client.cert.filename` is used. +client_sni_policy Outbound Policy of SNI on outbound connection. -client_key Outbound The file containing the client private key that corresponds to the certificate - for the outbound connection. + If not specified, the value of :ts:cv:`proxy.config.ssl.client.sni_policy` is used. - If this is relative, it is relative to the path in - :ts:cv:`proxy.config.ssl.client.private_key.path`. If not set, - |TS| tries to use a private key in client_cert. Otherwise, - :ts:cv:`proxy.config.ssl.client.private_key.filename` is used. +http2 Inbound Indicates whether the H2 protocol should be added to or removed from the + protocol negotiation list. The valid values are :code:`on` or :code:`off`. -client_sni_policy Outbound Policy of SNI on outbound connection. +http2_buffer_water_mark Inbound Specifies the high water mark for all HTTP/2 frames on an outoging connection. + By default this is :ts:cv:`proxy.config.http2.default_buffer_water_mark`. + NOTE: Connection coalescing may prevent this taking effect. - If not specified, the value of :ts:cv:`proxy.config.ssl.client.sni_policy` is used. +http2_max_settings_frames_per_minute Inbound Specifies how many SETTINGS frames |TS| receives per minute at maximum. + By default this is :ts:cv:`proxy.config.http2.max_settings_frames_per_minute`. + NOTE: Connection coalescing may prevent this from taking effect. -http2 Inbound Indicates whether the H2 protocol should be added to or removed from the - protocol negotiation list. The valid values are :code:`on` or :code:`off`. +http2_max_ping_frames_per_minute Inbound Specifies how many PING frames |TS| receives per minute at maximum. + By default this is :ts:cv:`proxy.config.http2.max_settings_frames_per_minute`. + NOTE: Connection coalescing may prevent this from taking effect. -http2_buffer_water_mark Inbound Specifies the high water mark for all HTTP/2 frames on an outoging connection. - By default this is :ts:cv:`proxy.config.http2.default_buffer_water_mark`. - NOTE: Connection coalescing may prevent this taking effect. +http2_max_priority_frames_per_minute Inbound Specifies how many PRIORITY frames |TS| receives per minute at maximum. + By default this is :ts:cv:`proxy.config.http2.max_settings_frames_per_minute`. + NOTE: Connection coalescing may prevent this from taking effect. -disable_h2 Inbound Deprecated for the more general h2 setting. Setting disable_h2 - to :code:`true` is the same as setting http2 to :code:`on`. +http2_max_rst_stream_frames_per_minute Inbound Specifies how many RST_STREAM frames |TS| receives per minute at maximum. + By default this is :ts:cv:`proxy.config.http2.max_settings_frames_per_minute`. + NOTE: Connection coalescing may prevent this from taking effect. -tunnel_route Inbound Destination as an FQDN and port, separated by a colon ``:``. - Match group number can be specified by ``$N`` where N should refer to a specified group - in the FQDN, ``tunnel_route: $1.domain``. +disable_h2 Inbound Deprecated for the more general h2 setting. Setting disable_h2 + to :code:`true` is the same as setting http2 to :code:`on`. - This will forward all traffic to the specified destination without first terminating - the incoming TLS connection. +tunnel_route Inbound Destination as an FQDN and port, separated by a colon ``:``. + Match group number can be specified by ``$N`` where N should refer to a specified group + in the FQDN, ``tunnel_route: $1.domain``. -forward_route Inbound Destination as an FQDN and port, separated by a colon ``:``. + This will forward all traffic to the specified destination without first terminating + the incoming TLS connection. - This is similar to tunnel_route, but it terminates the TLS connection and forwards the - decrypted traffic. |TS| will not interpret the decrypted data, so the contents do not - need to be HTTP. +forward_route Inbound Destination as an FQDN and port, separated by a colon ``:``. -partial_blind_route Inbound Destination as an FQDN and port, separated by a colon ``:``. + This is similar to tunnel_route, but it terminates the TLS connection and forwards the + decrypted traffic. |TS| will not interpret the decrypted data, so the contents do not + need to be HTTP. - This is similar to forward_route in that |TS| terminates the incoming TLS connection. - In addition partial_blind_route creates a new TLS connection to the specified origin. - It does not interpret the decrypted data before passing it to the origin TLS - connection, so the contents do not need to be HTTP. +partial_blind_route Inbound Destination as an FQDN and port, separated by a colon ``:``. -tunnel_alpn Inbound List of ALPN Protocol Ids for Partial Blind Tunnel. + This is similar to forward_route in that |TS| terminates the incoming TLS connection. + In addition partial_blind_route creates a new TLS connection to the specified origin. + It does not interpret the decrypted data before passing it to the origin TLS + connection, so the contents do not need to be HTTP. - ATS negotiates application protocol with the client on behalf of the origin server. - This only works with ``partial_blind_route``. -========================= ========= ======================================================================================== +tunnel_alpn Inbound List of ALPN Protocol Ids for Partial Blind Tunnel. + + ATS negotiates application protocol with the client on behalf of the origin server. + This only works with ``partial_blind_route``. +====================================== ========= ======================================================================================== Pre-warming TLS Tunnel ---------------------- 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 e00885c294..b22da8e1c6 100644 --- a/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst +++ b/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst @@ -260,6 +260,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 diff --git a/iocore/net/P_SNIActionPerformer.h b/iocore/net/P_SNIActionPerformer.h index 32d803dea0..e223ac7d0b 100644 --- a/iocore/net/P_SNIActionPerformer.h +++ b/iocore/net/P_SNIActionPerformer.h @@ -118,6 +118,74 @@ private: int value = -1; }; +class HTTP2MaxSettingsFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxSettingsFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxSettingsFramesPerMinute() override {} + + int + SNIAction(TLSSNISupport *snis, const Context &ctx) const override + { + snis->hints_from_sni.http2_max_settings_frames_per_minute = value; + return SSL_TLSEXT_ERR_OK; + } + +private: + int value = -1; +}; + +class HTTP2MaxPingFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxPingFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxPingFramesPerMinute() override {} + + int + SNIAction(TLSSNISupport *snis, const Context &ctx) const override + { + snis->hints_from_sni.http2_max_ping_frames_per_minute = value; + return SSL_TLSEXT_ERR_OK; + } + +private: + int value = -1; +}; + +class HTTP2MaxPriorityFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxPriorityFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxPriorityFramesPerMinute() override {} + + int + SNIAction(TLSSNISupport *snis, const Context &ctx) const override + { + snis->hints_from_sni.http2_max_priority_frames_per_minute = value; + return SSL_TLSEXT_ERR_OK; + } + +private: + int value = -1; +}; + +class HTTP2MaxRstStreamFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxRstStreamFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxRstStreamFramesPerMinute() override {} + + int + SNIAction(TLSSNISupport *snis, const Context &ctx) const override + { + snis->hints_from_sni.http2_max_rst_stream_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 0854251259..a7071013f6 100644 --- a/iocore/net/SSLSNIConfig.cc +++ b/iocore/net/SSLSNIConfig.cc @@ -138,6 +138,19 @@ SNIConfigParams::load_sni_config() if (item.http2_buffer_water_mark.has_value()) { ai->actions.push_back(std::make_unique<HTTP2BufferWaterMark>(item.http2_buffer_water_mark.value())); } + if (item.http2_max_settings_frames_per_minute.has_value()) { + ai->actions.push_back(std::make_unique<HTTP2MaxSettingsFramesPerMinute>(item.http2_max_settings_frames_per_minute.value())); + } + if (item.http2_max_ping_frames_per_minute.has_value()) { + ai->actions.push_back(std::make_unique<HTTP2MaxPingFramesPerMinute>(item.http2_max_ping_frames_per_minute.value())); + } + if (item.http2_max_priority_frames_per_minute.has_value()) { + ai->actions.push_back(std::make_unique<HTTP2MaxPriorityFramesPerMinute>(item.http2_max_priority_frames_per_minute.value())); + } + if (item.http2_max_rst_stream_frames_per_minute.has_value()) { + ai->actions.push_back( + std::make_unique<HTTP2MaxRstStreamFramesPerMinute>(item.http2_max_rst_stream_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 2f5d6467c2..ba2d13e930 100644 --- a/iocore/net/TLSSNISupport.h +++ b/iocore/net/TLSSNISupport.h @@ -52,6 +52,10 @@ public: struct HintsFromSNI { std::optional<uint32_t> http2_buffer_water_mark; + std::optional<uint32_t> http2_max_settings_frames_per_minute; + 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; } hints_from_sni; protected: diff --git a/iocore/net/YamlSNIConfig.cc b/iocore/net/YamlSNIConfig.cc index 3f90fe99ce..9a777b806f 100644 --- a/iocore/net/YamlSNIConfig.cc +++ b/iocore/net/YamlSNIConfig.cc @@ -144,6 +144,10 @@ std::set<std::string> valid_sni_config_keys = {TS_fqdn, TS_client_sni_policy, TS_http2, TS_http2_buffer_water_mark, + TS_http2_max_settings_frames_per_minute, + TS_http2_max_ping_frames_per_minute, + TS_http2_max_priority_frames_per_minute, + TS_http2_max_rst_stream_frames_per_minute, TS_ip_allow, #if TS_USE_HELLO_CB || defined(OPENSSL_IS_BORINGSSL) TS_valid_tls_versions_in, @@ -177,6 +181,18 @@ template <> struct convert<YamlSNIConfig::Item> { if (node[TS_http2_buffer_water_mark]) { item.http2_buffer_water_mark = node[TS_http2_buffer_water_mark].as<int>(); } + if (node[TS_http2_max_settings_frames_per_minute]) { + item.http2_max_settings_frames_per_minute = node[TS_http2_max_settings_frames_per_minute].as<int>(); + } + if (node[TS_http2_max_ping_frames_per_minute]) { + item.http2_max_ping_frames_per_minute = node[TS_http2_max_ping_frames_per_minute].as<int>(); + } + if (node[TS_http2_max_priority_frames_per_minute]) { + item.http2_max_priority_frames_per_minute = node[TS_http2_max_priority_frames_per_minute].as<int>(); + } + 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>(); + } // enum if (node[TS_verify_client]) { diff --git a/iocore/net/YamlSNIConfig.h b/iocore/net/YamlSNIConfig.h index dbb79ba298..b297bd5c16 100644 --- a/iocore/net/YamlSNIConfig.h +++ b/iocore/net/YamlSNIConfig.h @@ -56,6 +56,10 @@ TSDECL(ip_allow); TSDECL(valid_tls_versions_in); TSDECL(http2); TSDECL(http2_buffer_water_mark); +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(host_sni_policy); #undef TSDECL @@ -86,6 +90,10 @@ struct YamlSNIConfig { unsigned long protocol_mask; std::vector<int> tunnel_alpn{}; std::optional<int> http2_buffer_water_mark; + std::optional<int> http2_max_settings_frames_per_minute; + 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; bool tunnel_prewarm_srv = false; uint32_t tunnel_prewarm_min = 0; diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc index b547ece86e..433ff381b8 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -1391,6 +1391,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} diff --git a/proxy/http2/HTTP2.cc b/proxy/http2/HTTP2.cc index 5b263bbe75..04813d2212 100644 --- a/proxy/http2/HTTP2.cc +++ b/proxy/http2/HTTP2.cc @@ -83,6 +83,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"; static const char *const HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_IN_NAME = "proxy.process.http2.max_concurrent_streams_exceeded_in"; @@ -796,35 +798,36 @@ 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; -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; +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() @@ -849,6 +852,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"); @@ -917,6 +921,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); 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 dbc46744bc..5847865a9a 100644 --- a/proxy/http2/HTTP2.h +++ b/proxy/http2/HTTP2.h @@ -104,6 +104,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_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_IN, HTTP2_STAT_MAX_CONCURRENT_STREAMS_EXCEEDED_OUT, @@ -402,6 +403,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; diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc index dae2e97c88..888fdc39b5 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -28,6 +28,7 @@ #include "Http2Frame.h" #include "Http2DebugNames.h" #include "HttpDebugNames.h" +#include "TLSSNISupport.h" #include "tscpp/util/PostScript.h" #include "tscpp/util/LocalBuffer.h" @@ -465,8 +466,8 @@ rcv_priority_frame(Http2ConnectionState &cstate, const Http2Frame &frame) // Update PRIORITY frame count per minute cstate.increment_received_priority_frame_count(); // Close this connection if its priority frame count received exceeds a limit - if (Http2::max_priority_frames_per_minute != 0 && - cstate.get_received_priority_frame_count() > Http2::max_priority_frames_per_minute) { + if (cstate.configured_max_priority_frames_per_minute != 0 && + cstate.get_received_priority_frame_count() > cstate.configured_max_priority_frames_per_minute) { HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED, this_ethread()); Http2StreamDebug(cstate.session, stream_id, "Observed too frequent priority changes: %u priority changes within a last minute", cstate.get_received_priority_frame_count()); @@ -537,6 +538,18 @@ 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.configured_max_rst_stream_frames_per_minute != 0 && + cstate.get_received_rst_stream_frame_count() > cstate.configured_max_rst_stream_frames_per_minute) { + HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_RST_STREAM_FRAMES_PER_MINUTE_EXCEEDED, this_ethread()); + Http2StreamDebug(cstate.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. @@ -578,7 +591,8 @@ rcv_settings_frame(Http2ConnectionState &cstate, const Http2Frame &frame) // Update SETTIGNS frame count per minute cstate.increment_received_settings_frame_count(); // Close this connection if its SETTINGS frame count exceeds a limit - if (cstate.get_received_settings_frame_count() > Http2::max_settings_frames_per_minute) { + if (cstate.configured_max_settings_frames_per_minute != 0 && + cstate.get_received_settings_frame_count() > cstate.configured_max_settings_frames_per_minute) { HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_SETTINGS_FRAMES_PER_MINUTE_EXCEEDED, this_ethread()); Http2StreamDebug(cstate.session, stream_id, "Observed too frequent SETTINGS frames: %u frames within a last minute", cstate.get_received_settings_frame_count()); @@ -712,7 +726,8 @@ rcv_ping_frame(Http2ConnectionState &cstate, const Http2Frame &frame) // Update PING frame count per minute cstate.increment_received_ping_frame_count(); // Close this connection if its ping count received exceeds a limit - if (cstate.get_received_ping_frame_count() > Http2::max_ping_frames_per_minute) { + if (cstate.configured_max_ping_frames_per_minute != 0 && + cstate.get_received_ping_frame_count() > cstate.configured_max_ping_frames_per_minute) { HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_MAX_PING_FRAMES_PER_MINUTE_EXCEEDED, this_ethread()); Http2StreamDebug(cstate.session, stream_id, "Observed too frequent PING frames: %u PING frames within a last minute", cstate.get_received_ping_frame_count()); @@ -1073,6 +1088,25 @@ 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; + 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(); + } + if (snis->hints_from_sni.http2_max_ping_frames_per_minute.has_value()) { + configured_max_ping_frames_per_minute = snis->hints_from_sni.http2_max_ping_frames_per_minute.value(); + } + if (snis->hints_from_sni.http2_max_priority_frames_per_minute.has_value()) { + configured_max_priority_frames_per_minute = snis->hints_from_sni.http2_max_priority_frames_per_minute.value(); + } + 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(); + } + } + _cop = ActivityCop<Http2Stream>(this->mutex, &stream_list, 1); _cop.start(); } @@ -2094,6 +2128,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 diff --git a/proxy/http2/Http2ConnectionState.h b/proxy/http2/Http2ConnectionState.h index ca65a90846..76d2e2a8e1 100644 --- a/proxy/http2/Http2ConnectionState.h +++ b/proxy/http2/Http2ConnectionState.h @@ -102,6 +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; + void init(Http2CommonSession *ssn); void send_connection_preface(); void destroy(); @@ -167,6 +172,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); @@ -212,6 +219,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