From 80518252ba33b2daa7f0ebccf8c57b29555776f8 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 20 Sep 2023 17:00:47 -0600 Subject: [PATCH] Add an HTTP/2 related rate limiting --- doc/admin-guide/files/records.yaml.en.rst | 7 + doc/admin-guide/files/sni.yaml.en.rst | 374 +++++++++--------- .../statistics/core/http-connection.en.rst | 7 + iocore/net/P_SNIActionPerformer.h | 76 ++++ iocore/net/TLSSNISupport.h | 4 + iocore/net/YamlSNIConfig.cc | 28 ++ iocore/net/YamlSNIConfig.h | 8 + proxy/http2/HTTP2.cc | 34 +- proxy/http2/HTTP2.h | 2 + proxy/http2/Http2ConnectionState.cc | 52 ++- proxy/http2/Http2ConnectionState.h | 8 + src/records/RecordsConfig.cc | 2 + 12 files changed, 404 insertions(+), 198 deletions(-) diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index f30e8f87b66..0b707a93f5f 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -4532,6 +4532,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 f37fdc2ee67..0319697a76d 100644 --- a/doc/admin-guide/files/sni.yaml.en.rst +++ b/doc/admin-guide/files/sni.yaml.en.rst @@ -83,187 +83,203 @@ inbound_port_ranges Inbound The port ranges for the inbound connection i The following fields are the directives that determine the behavior of connections matching the key. -============================ ========= ======================================================================================== -Key Direction Meaning -============================ ========= ======================================================================================== -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-192.168.10.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. - - Alternatively, the path to a file containing - the list of IP addresses can be specified in - the form of ``"@path_to_file"``. The IP - addresses in the file can be either - comma-separated or line-separated. If a - given file path does not begin with ``/``, - it must be relative to the Traffic Server - configuration directory. Here is an example - showing this form of the configuration: - - ``ip_allow: "@ip_dir/example.com.ip.txt"`` - -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. - -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.verify.server.properties`. - This controls what |TS| checks when evaluating 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. - - By default this is :ts:cv:`proxy.config.ssl.client.certification_level`. - -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 begin - with ``/`` , it must be relative to the |TS| configuration directory. - ``verify_client_ca_certs`` can only be used with capabilities provided by - OpenSSL 1.0.2 or later. - -host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. - - 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 how this configuration behaves, see the corresponding - :ts:cv:`proxy.config.http.host_sni_policy` :file:`records.yaml` documentation. - - 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. - -valid_tls_version_min_in Inbound This specifies the minimum TLS version that will be offered to user agents during - the TLS negotiation. This replaces the global settings in - :ts:cv:`proxy.config.ssl.server.version.min`, - :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. 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. - -valid_tls_version_max_in Inbound This specifies the minimum TLS version that will be offered to user agents during - the TLS negotiation. This replaces the global settings in - :ts:cv:`proxy.config.ssl.server.version.max`, - :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. 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. - -valid_tls_versions_in Inbound Deprecated. 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_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.cert.path`. If not set - :ts:cv:`proxy.config.ssl.client.cert.filename` is used. - -client_key Outbound The file containing the client private key that corresponds to the certificate - 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. - -client_sni_policy Outbound Policy of SNI on outbound connection. - - If not specified, the value of :ts:cv:`proxy.config.ssl.client.sni_policy` 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`. - -http2_buffer_water_mark Inbound Specifies the high water mark for all HTTP/2 frames on an outgoing connection. - By default this is :ts:cv:`proxy.config.http2.default_buffer_water_mark`. - NOTE: Connection coalescing may prevent this from taking effect. - -http2_initial_window_size_in Inbound Specifies the initial HTTP/2 stream window size for inbound connections that - |TS| as a receiver advertises to the peer. - By default this is :ts:cv:`proxy.config.http2.initial_window_size_in`. - NOTE: Connection coalescing may prevent this from taking effect. - -quic Inbound Indicates whether QUIC connections should be accepted. The valid values are :code:`on` or - :code:`off`. Note that this is a more specific setting to configure QUIC availability per server - name. More broadly, you will also need to configure :ts:cv:`proxy.config.http.server_ports` to - open ports for QUIC. - -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``. - - This will forward all traffic to the specified destination without first terminating - the incoming TLS connection. - - The destination port can be designated with the literal string - ``{inbound_local_port}`` to specify that |TS| should connect to the tunnel route's - port on the same destination port that the incoming connection had. For - example, if a client connected to |TS| on port ``4443`` and the associated - ``tunnel_route`` had ``{inbound_local_port}`` for the port designation, then |TS| - will connect to the specified host using port ``4443``. - - The destination port can also be designated with the literal string - ``{proxy_protocol_port}``, in which case |TS| will connect to the specified host on - the port that was specified by the incoming Proxy Protocol payload. See :ref:`Proxy - Protocol ` for more information on Proxy Protocol and how it is - configured for |TS|. - - Note that only one of the ``{inbound_local_port}`` and ``{proxy_protocol_port}`` literal - strings can be specified. The match group number can be used in combination with either - one of those. - - For each of these tunnel targets, unless the port is explicitly specified in the target - (e.g., if the port is derived from the Proxy Protocol header), the port must be - specified in the :ts:cv:`proxy.config.http.connect_ports` configuration in order for - the tunnel to succeed. - -forward_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. - -partial_blind_route Inbound Destination as an FQDN and port, separated by a colon ``:``. - - 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. - -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``. +====================================== ========= ======================================================================================== +Key Direction Meaning +====================================== ========= ======================================================================================== +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-192.168.10.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. + + Alternatively, the path to a file containing + the list of IP addresses can be specified in + the form of ``"@path_to_file"``. The IP + addresses in the file can be either + comma-separated or line-separated. If a + given file path does not begin with ``/``, + it must be relative to the Traffic Server + configuration directory. Here is an example + showing this form of the configuration: + + ``ip_allow: "@ip_dir/example.com.ip.txt"`` + +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. + +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.verify.server.properties`. + This controls what |TS| checks when evaluating 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. + + By default this is :ts:cv:`proxy.config.ssl.client.certification_level`. + +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 begin + with ``/`` , it must be relative to the |TS| configuration directory. + ``verify_client_ca_certs`` can only be used with capabilities provided by + OpenSSL 1.0.2 or later. + +host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. + + 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 how this configuration behaves, see the corresponding + :ts:cv:`proxy.config.http.host_sni_policy` :file:`records.yaml` documentation. + + 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. + +valid_tls_version_min_in Inbound This specifies the minimum TLS version that will be offered to user agents during + the TLS negotiation. This replaces the global settings in + :ts:cv:`proxy.config.ssl.server.version.min`, + :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. 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. + +valid_tls_version_max_in Inbound This specifies the minimum TLS version that will be offered to user agents during + the TLS negotiation. This replaces the global settings in + :ts:cv:`proxy.config.ssl.server.version.max`, + :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. 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. + +valid_tls_versions_in Inbound Deprecated. 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_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.cert.path`. If not set + :ts:cv:`proxy.config.ssl.client.cert.filename` is used. + +client_key Outbound The file containing the client private key that corresponds to the certificate + 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. + +client_sni_policy Outbound Policy of SNI on outbound connection. + + If not specified, the value of :ts:cv:`proxy.config.ssl.client.sni_policy` 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`. + +http2_buffer_water_mark Inbound Specifies the high water mark for all HTTP/2 frames on an outgoing connection. + By default this is :ts:cv:`proxy.config.http2.default_buffer_water_mark`. + NOTE: Connection coalescing may prevent this from taking effect. + +http2_initial_window_size_in Inbound Specifies the initial HTTP/2 stream window size for inbound connections that + |TS| as a receiver advertises to the peer. + By default this is :ts:cv:`proxy.config.http2.initial_window_size_in`. + NOTE: Connection coalescing may prevent this from taking effect. + +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_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_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. + +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. + +quic Inbound Indicates whether QUIC connections should be accepted. The valid values are :code:`on` or + :code:`off`. Note that this is a more specific setting to configure QUIC availability per server + name. More broadly, you will also need to configure :ts:cv:`proxy.config.http.server_ports` to + open ports for QUIC. + +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``. + + This will forward all traffic to the specified destination without first terminating + the incoming TLS connection. + + The destination port can be designated with the literal string + ``{inbound_local_port}`` to specify that |TS| should connect to the tunnel route's + port on the same destination port that the incoming connection had. For + example, if a client connected to |TS| on port ``4443`` and the associated + ``tunnel_route`` had ``{inbound_local_port}`` for the port designation, then |TS| + will connect to the specified host using port ``4443``. + + The destination port can also be designated with the literal string + ``{proxy_protocol_port}``, in which case |TS| will connect to the specified host on + the port that was specified by the incoming Proxy Protocol payload. See :ref:`Proxy + Protocol ` for more information on Proxy Protocol and how it is + configured for |TS|. + + Note that only one of the ``{inbound_local_port}`` and ``{proxy_protocol_port}`` literal + strings can be specified. The match group number can be used in combination with either + one of those. + + For each of these tunnel targets, unless the port is explicitly specified in the target + (e.g., if the port is derived from the Proxy Protocol header), the port must be + specified in the :ts:cv:`proxy.config.http.connect_ports` configuration in order for + the tunnel to succeed. -server_max_early_data Inbound Specifies the maximum amount of early data in bytes that is permitted to be sent on a single connection. +forward_route Inbound Destination as an FQDN and port, separated by a colon ``:``. - If not specified, the value of :ts:cv:`proxy.config.ssl.server.max_early_data` is used. -============================ ========= ======================================================================================== + 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. + +partial_blind_route Inbound Destination as an FQDN and port, separated by a colon ``:``. + + 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. + +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``. + +server_max_early_data Inbound Specifies the maximum amount of early data in bytes that is permitted to be sent on a single connection. + + If not specified, the value of :ts:cv:`proxy.config.ssl.server.max_early_data` is used. +====================================== ========= ======================================================================================== 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 e19295a4e74..28de812a0bf 100644 --- a/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst +++ b/doc/admin-guide/monitoring/statistics/core/http-connection.en.rst @@ -280,6 +280,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 64fc4a9566a..53c0b2ebcca 100644 --- a/iocore/net/P_SNIActionPerformer.h +++ b/iocore/net/P_SNIActionPerformer.h @@ -130,6 +130,82 @@ class HTTP2InitialWindowSizeIn : public ActionItem int value = -1; }; +class HTTP2MaxSettingsFramesPerMinute : public ActionItem +{ +public: + HTTP2MaxSettingsFramesPerMinute(int value) : value(value) {} + ~HTTP2MaxSettingsFramesPerMinute() override {} + + int + SNIAction(SSL &ssl, const Context &ctx) const override + { + if (auto snis = TLSSNISupport::getInstance(&ssl)) { + 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(SSL &ssl, const Context &ctx) const override + { + if (auto snis = TLSSNISupport::getInstance(&ssl)) { + 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(SSL &ssl, const Context &ctx) const override + { + if (auto snis = TLSSNISupport::getInstance(&ssl)) { + 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(SSL &ssl, const Context &ctx) const override + { + if (auto snis = TLSSNISupport::getInstance(&ssl)) { + 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 { // ID of the configured variable. This will be used to know which function diff --git a/iocore/net/TLSSNISupport.h b/iocore/net/TLSSNISupport.h index 3263760fec5..c13bd6f63e9 100644 --- a/iocore/net/TLSSNISupport.h +++ b/iocore/net/TLSSNISupport.h @@ -60,6 +60,10 @@ class TLSSNISupport std::optional http2_buffer_water_mark; std::optional server_max_early_data; std::optional http2_initial_window_size_in; + std::optional http2_max_settings_frames_per_minute; + std::optional http2_max_ping_frames_per_minute; + std::optional http2_max_priority_frames_per_minute; + std::optional http2_max_rst_stream_frames_per_minute; std::optional outbound_sni_policy; } hints_from_sni; diff --git a/iocore/net/YamlSNIConfig.cc b/iocore/net/YamlSNIConfig.cc index 063960d77aa..3be51c3e9f8 100644 --- a/iocore/net/YamlSNIConfig.cc +++ b/iocore/net/YamlSNIConfig.cc @@ -161,6 +161,18 @@ YamlSNIConfig::Item::populate_sni_actions(action_vector_t &actions) if (http2_initial_window_size_in.has_value()) { actions.push_back(std::make_unique(http2_initial_window_size_in.value())); } + if (http2_max_settings_frames_per_minute.has_value()) { + actions.push_back(std::make_unique(http2_max_settings_frames_per_minute.value())); + } + if (http2_max_ping_frames_per_minute.has_value()) { + actions.push_back(std::make_unique(http2_max_ping_frames_per_minute.value())); + } + if (http2_max_priority_frames_per_minute.has_value()) { + actions.push_back(std::make_unique(http2_max_priority_frames_per_minute.value())); + } + if (http2_max_rst_stream_frames_per_minute.has_value()) { + actions.push_back(std::make_unique(http2_max_rst_stream_frames_per_minute.value())); + } actions.push_back(std::make_unique(server_max_early_data)); actions.push_back(std::make_unique(ip_allow, fqdn)); @@ -204,6 +216,10 @@ std::set valid_sni_config_keys = {TS_fqdn, TS_http2, TS_http2_buffer_water_mark, TS_http2_initial_window_size_in, + 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_quic, TS_ip_allow, #if TS_USE_HELLO_CB || defined(OPENSSL_IS_BORINGSSL) @@ -247,6 +263,18 @@ template <> struct convert { if (node[TS_http2_initial_window_size_in]) { item.http2_initial_window_size_in = node[TS_http2_initial_window_size_in].as(); } + 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(); + } + 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(); + } + 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(); + } + 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(); + } if (node[TS_quic]) { item.offer_quic = node[TS_quic].as(); } diff --git a/iocore/net/YamlSNIConfig.h b/iocore/net/YamlSNIConfig.h index 510ca50a734..90043c136ff 100644 --- a/iocore/net/YamlSNIConfig.h +++ b/iocore/net/YamlSNIConfig.h @@ -64,6 +64,10 @@ TSDECL(valid_tls_version_min_in); TSDECL(valid_tls_version_max_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(quic); TSDECL(host_sni_policy); TSDECL(http2_initial_window_size_in); @@ -103,6 +107,10 @@ struct YamlSNIConfig { int valid_tls_version_max_in = -1; std::vector tunnel_alpn{}; std::optional http2_buffer_water_mark; + std::optional http2_max_settings_frames_per_minute; + std::optional http2_max_ping_frames_per_minute; + std::optional http2_max_priority_frames_per_minute; + std::optional http2_max_rst_stream_frames_per_minute; uint32_t server_max_early_data = 0; std::optional http2_initial_window_size_in; diff --git a/proxy/http2/HTTP2.cc b/proxy/http2/HTTP2.cc index 9ee035d2377..becd55b1992 100644 --- a/proxy/http2/HTTP2.cc +++ b/proxy/http2/HTTP2.cc @@ -480,21 +480,22 @@ uint32_t Http2::initial_window_size_out = 65535; Http2FlowControlPolicy Http2::flow_control_policy_out = Http2FlowControlPolicy::STATIC_SESSION_AND_STATIC_STREAM; uint32_t Http2::no_activity_timeout_out = 120; -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; +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() @@ -541,6 +542,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"); @@ -596,6 +598,8 @@ Http2::init() http2_rsb.max_ping_frames_per_minute_exceeded = intm.newMetricPtr("proxy.process.http2.max_ping_frames_per_minute_exceeded"); http2_rsb.max_priority_frames_per_minute_exceeded = intm.newMetricPtr("proxy.process.http2.max_priority_frames_per_minute_exceeded"); + http2_rsb.max_rst_stream_frames_per_minute_exceeded = + intm.newMetricPtr("proxy.process.http2.max_rst_stream_frames_per_minute_exceeded"); http2_rsb.insufficient_avg_window_update = intm.newMetricPtr("proxy.process.http2.insufficient_avg_window_update"); http2_rsb.max_concurrent_streams_exceeded_in = intm.newMetricPtr("proxy.process.http2.max_concurrent_streams_exceeded_in"); http2_rsb.max_concurrent_streams_exceeded_out = intm.newMetricPtr("proxy.process.http2.max_concurrent_streams_exceeded_out"); diff --git a/proxy/http2/HTTP2.h b/proxy/http2/HTTP2.h index 2c65a6b323a..8bb8868adf8 100644 --- a/proxy/http2/HTTP2.h +++ b/proxy/http2/HTTP2.h @@ -102,6 +102,7 @@ struct Http2StatsBlock { Metrics::IntType *max_settings_frames_per_minute_exceeded; Metrics::IntType *max_ping_frames_per_minute_exceeded; Metrics::IntType *max_priority_frames_per_minute_exceeded; + Metrics::IntType *max_rst_stream_frames_per_minute_exceeded; Metrics::IntType *insufficient_avg_window_update; Metrics::IntType *max_concurrent_streams_exceeded_in; Metrics::IntType *max_concurrent_streams_exceeded_out; @@ -412,6 +413,7 @@ class Http2 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 05ac31dcb20..4d5a4a511af 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -551,8 +551,8 @@ Http2ConnectionState::rcv_priority_frame(const Http2Frame &frame) // Update PRIORITY frame count per minute this->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 && - this->get_received_priority_frame_count() > Http2::max_priority_frames_per_minute) { + if (configured_max_priority_frames_per_minute != 0 && + this->get_received_priority_frame_count() > configured_max_priority_frames_per_minute) { Metrics::increment(http2_rsb.max_priority_frames_per_minute_exceeded); Http2StreamDebug(this->session, stream_id, "Observed too frequent priority changes: %u priority changes within a last minute", this->get_received_priority_frame_count()); @@ -623,6 +623,18 @@ Http2ConnectionState::rcv_rst_stream_frame(const Http2Frame &frame) "reset frame wrong length"); } + // Update RST_STREAM frame count per minute + this->increment_received_rst_stream_frame_count(); + // Close this connection if its RST_STREAM frame count exceeds a limit + if (configured_max_rst_stream_frames_per_minute != 0 && + this->get_received_rst_stream_frame_count() > configured_max_rst_stream_frames_per_minute) { + Metrics::increment(http2_rsb.max_rst_stream_frames_per_minute_exceeded); + Http2StreamDebug(this->session, stream_id, "Observed too frequent RST_STREAM frames: %u frames within a last minute", + this->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. @@ -663,7 +675,8 @@ Http2ConnectionState::rcv_settings_frame(const Http2Frame &frame) // Update SETTINGS frame count per minute this->increment_received_settings_frame_count(); // Close this connection if its SETTINGS frame count exceeds a limit - if (this->get_received_settings_frame_count() > Http2::max_settings_frames_per_minute) { + if (configured_max_settings_frames_per_minute != 0 && + this->get_received_settings_frame_count() > configured_max_settings_frames_per_minute) { Metrics::increment(http2_rsb.max_settings_frames_per_minute_exceeded); Http2StreamDebug(this->session, stream_id, "Observed too frequent SETTINGS frames: %u frames within a last minute", this->get_received_settings_frame_count()); @@ -798,7 +811,7 @@ Http2ConnectionState::rcv_ping_frame(const Http2Frame &frame) // Update PING frame count per minute this->increment_received_ping_frame_count(); // Close this connection if its ping count received exceeds a limit - if (this->get_received_ping_frame_count() > Http2::max_ping_frames_per_minute) { + if (configured_max_ping_frames_per_minute != 0 && this->get_received_ping_frame_count() > configured_max_ping_frames_per_minute) { Metrics::increment(http2_rsb.max_ping_frames_per_minute_exceeded); Http2StreamDebug(this->session, stream_id, "Observed too frequent PING frames: %u PING frames within a last minute", this->get_received_ping_frame_count()); @@ -1220,6 +1233,25 @@ Http2ConnectionState::init(Http2CommonSession *ssn) dependency_tree = new DependencyTree(this->_get_configured_max_concurrent_streams()); } + 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 = session->get_netvc()->get_service(); 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(this->mutex, &stream_list, 1); _cop.start(); } @@ -2576,6 +2608,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 44c0cbe8f71..863adb92753 100644 --- a/proxy/http2/Http2ConnectionState.h +++ b/proxy/http2/Http2ConnectionState.h @@ -194,6 +194,8 @@ class Http2ConnectionState : public Continuation 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 get_peer_rwnd() const; Http2ErrorCode increment_peer_rwnd(size_t amount); @@ -327,6 +329,7 @@ class Http2ConnectionState : public Continuation Http2FrequencyCounter _received_settings_frame_counter; Http2FrequencyCounter _received_ping_frame_counter; Http2FrequencyCounter _received_priority_frame_counter; + Http2FrequencyCounter _received_rst_stream_frame_counter; /** Records the various settings for each SETTINGS frame that we've sent. * @@ -393,6 +396,11 @@ class Http2ConnectionState : public Continuation Event *shutdown_cont_event = nullptr; Event *fini_event = nullptr; Event *zombie_event = nullptr; + + 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; }; /////////////////////////////////////////////// diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index 1a5ecfa3ab4..b4c5c395cb3 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -1315,6 +1315,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}