This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/8.0.x by this push:
     new 65e3804  HTTP/2 rate limiting
65e3804 is described below

commit 65e3804816b47588cf73d37ca806f32b8b795b4a
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Tue Aug 13 09:57:20 2019 -0700

    HTTP/2 rate limiting
---
 doc/admin-guide/files/records.config.en.rst |  28 ++++
 iocore/eventsystem/I_VIO.h                  |   6 +
 iocore/eventsystem/P_VIO.h                  |  14 ++
 iocore/net/SSLNetVConnection.cc             |   5 +-
 iocore/net/UnixNetVConnection.cc            |   2 +-
 mgmt/RecordsConfig.cc                       |   8 ++
 proxy/http2/HTTP2.cc                        |  42 +++---
 proxy/http2/HTTP2.h                         |   4 +
 proxy/http2/Http2ClientSession.cc           |  53 +++++++-
 proxy/http2/Http2ClientSession.h            |   6 +
 proxy/http2/Http2ConnectionState.cc         | 204 ++++++++++++++++++++++++----
 proxy/http2/Http2ConnectionState.h          |  32 ++++-
 proxy/http2/Http2Stream.cc                  |  59 ++++++++
 proxy/http2/Http2Stream.h                   |  26 ++--
 14 files changed, 429 insertions(+), 60 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst 
b/doc/admin-guide/files/records.config.en.rst
index 2e3c944..555edac 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -3539,6 +3539,34 @@ HTTP/2 Configuration
    Clients exceeded this limit will be immediately disconnected with an error
    code of ENHANCE_YOUR_CALM.
 
+.. ts:cv:: CONFIG proxy.config.http2.max_settings_frames_per_minute INT 14
+   :reloadable:
+
+   Specifies how many SETTINGS 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.max_ping_frames_per_minute INT 60
+   :reloadable:
+
+   Specifies how many number of PING 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.max_priority_frames_per_minute INT 120
+   :reloadable:
+
+   Specifies how many number of PRIORITY 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:
+
+   Specifies the minimum average window increment |TS| allows. The average 
will be calculated based on the last 5 WINDOW_UPDATE frames.
+   Clients that send smaller window increments lower than this limit will be 
immediately disconnected with an error
+   code of ENHANCE_YOUR_CALM.
+
 Plug-in Configuration
 =====================
 
diff --git a/iocore/eventsystem/I_VIO.h b/iocore/eventsystem/I_VIO.h
index 6e7748c..de7ab32 100644
--- a/iocore/eventsystem/I_VIO.h
+++ b/iocore/eventsystem/I_VIO.h
@@ -139,6 +139,9 @@ public:
   */
   inkcoreapi void reenable_re();
 
+  void disable();
+  bool is_disabled();
+
   VIO(int aop);
   VIO();
 
@@ -218,6 +221,9 @@ public:
 
   */
   Ptr<ProxyMutex> mutex;
+
+private:
+  bool _disabled = false;
 };
 
 #include "I_VConnection.h"
diff --git a/iocore/eventsystem/P_VIO.h b/iocore/eventsystem/P_VIO.h
index d8fcb05..e2bf54f 100644
--- a/iocore/eventsystem/P_VIO.h
+++ b/iocore/eventsystem/P_VIO.h
@@ -104,6 +104,7 @@ VIO::set_continuation(Continuation *acont)
 TS_INLINE void
 VIO::reenable()
 {
+  this->_disabled = false;
   if (vc_server) {
     vc_server->reenable(this);
   }
@@ -117,7 +118,20 @@ VIO::reenable()
 TS_INLINE void
 VIO::reenable_re()
 {
+  this->_disabled = false;
   if (vc_server) {
     vc_server->reenable_re(this);
   }
 }
+
+TS_INLINE void
+VIO::disable()
+{
+  this->_disabled = true;
+}
+
+TS_INLINE bool
+VIO::is_disabled()
+{
+  return this->_disabled;
+}
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index f9500c1..a03022c 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -526,7 +526,7 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread 
*lthread)
   // If it is not enabled, lower its priority.  This allows
   // a fast connection to speed match a slower connection by
   // shifting down in priority even if it could read.
-  if (!s->enabled || s->vio.op != VIO::READ) {
+  if (!s->enabled || s->vio.op != VIO::READ || s->vio.is_disabled()) {
     read_disable(nh, this);
     return;
   }
@@ -632,7 +632,7 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread 
*lthread)
   }
 
   // If there is nothing to do or no space available, disable connection
-  if (ntodo <= 0 || !buf.writer()->write_avail()) {
+  if (ntodo <= 0 || !buf.writer()->write_avail() || s->vio.is_disabled()) {
     read_disable(nh, this);
     return;
   }
@@ -1577,6 +1577,7 @@ SSLNetVConnection::reenable(NetHandler *nh)
     }
     Debug("ssl", "iterate from reenable curHook=%p %d", curHook, 
sslHandshakeHookState);
   }
+
   this->readReschedule(nh);
 }
 
diff --git a/iocore/net/UnixNetVConnection.cc b/iocore/net/UnixNetVConnection.cc
index ec8b39f..3d76002 100644
--- a/iocore/net/UnixNetVConnection.cc
+++ b/iocore/net/UnixNetVConnection.cc
@@ -201,7 +201,7 @@ read_from_net(NetHandler *nh, UnixNetVConnection *vc, 
EThread *thread)
     return;
   }
   // if it is not enabled.
-  if (!s->enabled || s->vio.op != VIO::READ) {
+  if (!s->enabled || s->vio.op != VIO::READ || s->vio.is_disabled()) {
     read_disable(nh, vc);
     return;
   }
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index b922f72..df38162 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -1330,6 +1330,14 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http2.max_settings_per_minute", RECD_INT, "14", 
RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.http2.max_settings_frames_per_minute", RECD_INT, 
"14", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
+  ,
+  {RECT_CONFIG, "proxy.config.http2.max_ping_frames_per_minute", RECD_INT, 
"60", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
+  ,
+  {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.min_avg_window_update", RECD_FLOAT, 
"2560.0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
 
   //# Add LOCAL Records Here
   {RECT_LOCAL, "proxy.local.incoming_ip_to_bind", RECD_STRING, nullptr, 
RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/proxy/http2/HTTP2.cc b/proxy/http2/HTTP2.cc
index 675f621..d1e9884 100644
--- a/proxy/http2/HTTP2.cc
+++ b/proxy/http2/HTTP2.cc
@@ -719,23 +719,27 @@ 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        = 1048576;
-uint32_t Http2::max_frame_size             = 16384;
-uint32_t Http2::header_table_size          = 4096;
-uint32_t Http2::max_header_list_size       = 4294967295;
-uint32_t Http2::accept_no_activity_timeout = 120;
-uint32_t Http2::no_activity_timeout_in     = 120;
-uint32_t Http2::active_timeout_in          = 0;
-uint32_t Http2::push_diary_size            = 256;
-uint32_t Http2::zombie_timeout_in          = 0;
-float Http2::stream_error_rate_threshold   = 0.1;
-uint32_t Http2::max_settings_per_frame     = 7;
-uint32_t Http2::max_settings_per_minute    = 14;
+uint32_t Http2::max_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            = 1048576;
+uint32_t Http2::max_frame_size                 = 16384;
+uint32_t Http2::header_table_size              = 4096;
+uint32_t Http2::max_header_list_size           = 4294967295;
+uint32_t Http2::accept_no_activity_timeout     = 120;
+uint32_t Http2::no_activity_timeout_in         = 120;
+uint32_t Http2::active_timeout_in              = 0;
+uint32_t Http2::push_diary_size                = 256;
+uint32_t Http2::zombie_timeout_in              = 0;
+float Http2::stream_error_rate_threshold       = 0.1;
+uint32_t Http2::max_settings_per_frame         = 7;
+uint32_t Http2::max_settings_per_minute        = 14;
+uint32_t Http2::max_settings_frames_per_minute = 14;
+uint32_t Http2::max_ping_frames_per_minute     = 60;
+uint32_t Http2::max_priority_frames_per_minute = 120;
+float Http2::min_avg_window_update             = 2560.0;
 
 void
 Http2::init()
@@ -756,6 +760,10 @@ Http2::init()
   REC_EstablishStaticConfigFloat(stream_error_rate_threshold, 
"proxy.config.http2.stream_error_rate_threshold");
   REC_EstablishStaticConfigInt32U(max_settings_per_frame, 
"proxy.config.http2.max_settings_per_frame");
   REC_EstablishStaticConfigInt32U(max_settings_per_minute, 
"proxy.config.http2.max_settings_per_minute");
+  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_EstablishStaticConfigFloat(min_avg_window_update, 
"proxy.config.http2.min_avg_window_update");
 
   // If any settings is broken, ATS should not start
   
ink_release_assert(http2_settings_parameter_is_valid({HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
 max_concurrent_streams_in}));
diff --git a/proxy/http2/HTTP2.h b/proxy/http2/HTTP2.h
index d7203d2..504b899 100644
--- a/proxy/http2/HTTP2.h
+++ b/proxy/http2/HTTP2.h
@@ -381,6 +381,10 @@ public:
   static float stream_error_rate_threshold;
   static uint32_t max_settings_per_frame;
   static uint32_t max_settings_per_minute;
+  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 float min_avg_window_update;
 
   static void init();
 };
diff --git a/proxy/http2/Http2ClientSession.cc 
b/proxy/http2/Http2ClientSession.cc
index 6ee435e..8b68db3 100644
--- a/proxy/http2/Http2ClientSession.cc
+++ b/proxy/http2/Http2ClientSession.cc
@@ -74,6 +74,11 @@ Http2ClientSession::destroy()
 void
 Http2ClientSession::free()
 {
+  if (this->_reenable_event) {
+    this->_reenable_event->cancel();
+    this->_reenable_event = nullptr;
+  }
+
   if (h2_pushed_urls) {
     this->h2_pushed_urls = ink_hash_table_destroy(this->h2_pushed_urls);
   }
@@ -338,6 +343,13 @@ Http2ClientSession::main_event_handler(int event, void 
*edata)
     break;
   }
 
+  case HTTP2_SESSION_EVENT_REENABLE:
+    // VIO will be reenableed in this handler
+    retval = (this->*session_handler)(VC_EVENT_READ_READY, static_cast<VIO 
*>(e->cookie));
+    // Clear the event after calling session_handler to not reschedule 
REENABLE in it
+    this->_reenable_event = nullptr;
+    break;
+
   case VC_EVENT_ACTIVE_TIMEOUT:
   case VC_EVENT_INACTIVITY_TIMEOUT:
   case VC_EVENT_ERROR:
@@ -502,7 +514,16 @@ Http2ClientSession::state_complete_frame_read(int event, 
void *edata)
   STATE_ENTER(&Http2ClientSession::state_complete_frame_read, event);
   ink_assert(event == VC_EVENT_READ_COMPLETE || event == VC_EVENT_READ_READY);
   if (this->sm_reader->read_avail() < this->current_hdr.length) {
-    vio->reenable();
+    if (this->_should_do_something_else()) {
+      if (this->_reenable_event == nullptr) {
+        vio->disable();
+        this->_reenable_event = mutex->thread_holding->schedule_in(this, 
HRTIME_MSECONDS(1), HTTP2_SESSION_EVENT_REENABLE, vio);
+      } else {
+        vio->reenable();
+      }
+    } else {
+      vio->reenable();
+    }
     return 0;
   }
   Http2SsnDebug("completed frame read, %" PRId64 " bytes available", 
this->sm_reader->read_avail());
@@ -519,6 +540,7 @@ Http2ClientSession::do_complete_frame_read()
   Http2Frame frame(this->current_hdr, this->sm_reader);
   send_connection_event(&this->connection_state, HTTP2_SESSION_EVENT_RECV, 
&frame);
   this->sm_reader->consume(this->current_hdr.length);
+  ++(this->_n_frame_read);
 
   // Set the event handler if there is no more data to process a new frame
   HTTP2_SET_SESSION_HANDLER(&Http2ClientSession::state_start_frame_read);
@@ -539,9 +561,19 @@ Http2ClientSession::state_process_frame_read(int event, 
VIO *vio, bool inside_fr
       Http2SsnDebug("reading a frame has been canceled (%u)", 
connection_state.tx_error_code.code);
       break;
     }
+
+    Http2ErrorCode err = Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+    if (this->connection_state.get_stream_error_rate() > std::min(1.0, 
Http2::stream_error_rate_threshold * 2.0)) {
+      ip_port_text_buffer ipb;
+      const char *client_ip = ats_ip_ntop(get_client_addr(), ipb, sizeof(ipb));
+      Error("HTTP/2 session error client_ip=%s session_id=%" PRId64
+            " closing a connection, because its stream error rate (%f) is too 
high",
+            client_ip, connection_id(), 
this->connection_state.get_stream_error_rate());
+      err = Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM;
+    }
+
     // Return if there was an error
-    Http2ErrorCode err;
-    if (do_start_frame_read(err) < 0) {
+    if (err > Http2ErrorCode::HTTP2_ERROR_NO_ERROR || do_start_frame_read(err) 
< 0) {
       // send an error if specified.  Otherwise, just go away
       if (err > Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
         SCOPED_MUTEX_LOCK(lock, this->connection_state.mutex, this_ethread());
@@ -560,6 +592,14 @@ Http2ClientSession::state_process_frame_read(int event, 
VIO *vio, bool inside_fr
       break;
     }
     do_complete_frame_read();
+
+    if (this->_should_do_something_else()) {
+      if (this->_reenable_event == nullptr) {
+        vio->disable();
+        this->_reenable_event = mutex->thread_holding->schedule_in(this, 
HRTIME_MSECONDS(1), HTTP2_SESSION_EVENT_REENABLE, vio);
+        return 0;
+      }
+    }
   }
 
   // If the client hasn't shut us down, reenable
@@ -580,3 +620,10 @@ 
Http2ClientSession::decrement_current_active_client_connections_stat()
 {
   
HTTP2_DECREMENT_THREAD_DYN_STAT(HTTP2_STAT_CURRENT_ACTIVE_CLIENT_CONNECTION_COUNT,
 this_ethread());
 }
+
+bool
+Http2ClientSession::_should_do_something_else()
+{
+  // Do something else every 128 incoming frames
+  return (this->_n_frame_read & 0x7F) == 0;
+}
diff --git a/proxy/http2/Http2ClientSession.h b/proxy/http2/Http2ClientSession.h
index d917dae..e8b43a5 100644
--- a/proxy/http2/Http2ClientSession.h
+++ b/proxy/http2/Http2ClientSession.h
@@ -42,6 +42,7 @@
 #define HTTP2_SESSION_EVENT_XMIT (HTTP2_SESSION_EVENTS_START + 4)
 #define HTTP2_SESSION_EVENT_SHUTDOWN_INIT (HTTP2_SESSION_EVENTS_START + 5)
 #define HTTP2_SESSION_EVENT_SHUTDOWN_CONT (HTTP2_SESSION_EVENTS_START + 6)
+#define HTTP2_SESSION_EVENT_REENABLE (HTTP2_SESSION_EVENTS_START + 7)
 
 enum class Http2SessionCod : int {
   NOT_PROVIDED,
@@ -332,6 +333,8 @@ private:
   // if there are multiple frames ready on the wire
   int state_process_frame_read(int event, VIO *vio, bool inside_frame);
 
+  bool _should_do_something_else();
+
   int64_t total_write_len        = 0;
   SessionHandler session_handler = nullptr;
   NetVConnection *client_vc      = nullptr;
@@ -356,6 +359,9 @@ private:
 
   InkHashTable *h2_pushed_urls = nullptr;
   uint32_t h2_pushed_urls_size = 0;
+
+  Event *_reenable_event = nullptr;
+  int _n_frame_read      = 0;
 };
 
 extern ClassAllocator<Http2ClientSession> http2ClientSessionAllocator;
diff --git a/proxy/http2/Http2ConnectionState.cc 
b/proxy/http2/Http2ConnectionState.cc
index 6aad485..b300ae6 100644
--- a/proxy/http2/Http2ConnectionState.cc
+++ b/proxy/http2/Http2ConnectionState.cc
@@ -27,6 +27,7 @@
 #include "Http2Stream.h"
 #include "Http2DebugNames.h"
 #include <sstream>
+#include <numeric>
 
 #define Http2ConDebug(ua_session, fmt, ...) \
   SsnDebug(ua_session, "http2_con", "[%" PRId64 "] " fmt, 
ua_session->connection_id(), ##__VA_ARGS__);
@@ -137,18 +138,18 @@ rcv_data_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
   }
 
   // Check whether Window Size is acceptable
-  if (cstate.server_rwnd < payload_length) {
+  if (cstate.server_rwnd() < payload_length) {
     return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_FLOW_CONTROL_ERROR,
                       "recv data cstate.server_rwnd < payload_length");
   }
-  if (stream->server_rwnd < payload_length) {
+  if (stream->server_rwnd() < payload_length) {
     return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, 
Http2ErrorCode::HTTP2_ERROR_FLOW_CONTROL_ERROR,
                       "recv data stream->server_rwnd < payload_length");
   }
 
   // Update Window size
-  cstate.server_rwnd -= payload_length;
-  stream->server_rwnd -= payload_length;
+  cstate.decrement_server_rwnd(payload_length);
+  stream->decrement_server_rwnd(payload_length);
 
   const uint32_t unpadded_length = payload_length - pad_length;
   // If we call write() multiple times, we must keep the same reader, so we can
@@ -174,15 +175,15 @@ rcv_data_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
   uint32_t initial_rwnd = 
cstate.server_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
   uint32_t min_rwnd     = std::min(initial_rwnd, 
cstate.server_settings.get(HTTP2_SETTINGS_MAX_FRAME_SIZE));
   // Connection level WINDOW UPDATE
-  if (cstate.server_rwnd <= min_rwnd) {
-    Http2WindowSize diff_size = initial_rwnd - cstate.server_rwnd;
-    cstate.server_rwnd += diff_size;
+  if (cstate.server_rwnd() <= min_rwnd) {
+    Http2WindowSize diff_size = initial_rwnd - cstate.server_rwnd();
+    cstate.increment_server_rwnd(diff_size);
     cstate.send_window_update_frame(0, diff_size);
   }
   // Stream level WINDOW UPDATE
-  if (stream->server_rwnd <= min_rwnd) {
-    Http2WindowSize diff_size = initial_rwnd - stream->server_rwnd;
-    stream->server_rwnd += diff_size;
+  if (stream->server_rwnd() <= min_rwnd) {
+    Http2WindowSize diff_size = initial_rwnd - stream->server_rwnd();
+    stream->increment_server_rwnd(diff_size);
     cstate.send_window_update_frame(stream->get_id(), diff_size);
   }
 
@@ -406,6 +407,17 @@ rcv_priority_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
                       "PRIORITY frame depends on itself");
   }
 
+  // Update PRIORITY frame count per minute
+  cstate.increment_received_priority_frame_count();
+  // Close this conection if its priority frame count received exceeds a limit
+  if (cstate.get_received_priority_frame_count() > 
Http2::max_priority_frames_per_minute) {
+    Http2StreamDebug(cstate.ua_session, stream_id,
+                     "Observed too frequent priority changes: %u priority 
changes within a last minute",
+                     cstate.get_received_priority_frame_count());
+    return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
+                      "recv priority too frequent priority changes");
+  }
+
   if (!Http2::stream_priority_enabled) {
     return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE);
   }
@@ -511,6 +523,16 @@ rcv_settings_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
     Warning("Setting frame for zombied sessoin %" PRId64, 
cstate.ua_session->connection_id());
   }
 
+  // Update SETTIGNS frame count per minute
+  cstate.increment_received_settings_frame_count();
+  // Close this conection if its SETTINGS frame count exceeds a limit
+  if (cstate.get_received_settings_frame_count() > 
Http2::max_settings_frames_per_minute) {
+    Http2StreamDebug(cstate.ua_session, stream_id, "Observed too frequent 
SETTINGS 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,
+                      "recv settings too frequent SETTINGS frames");
+  }
+
   // [RFC 7540] 6.5. The stream identifier for a SETTINGS frame MUST be zero.
   // If an endpoint receives a SETTINGS frame whose stream identifier field is
   // anything other than 0x0, the endpoint MUST respond with a connection
@@ -632,6 +654,16 @@ rcv_ping_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
                       "ping bad length");
   }
 
+  // Update PING frame count per minute
+  cstate.increment_received_ping_frame_count();
+  // Close this conection if its ping count received exceeds a limit
+  if (cstate.get_received_ping_frame_count() > 
Http2::max_ping_frames_per_minute) {
+    Http2StreamDebug(cstate.ua_session, stream_id, "Observed too frequent PING 
frames: %u PING frames within a last minute",
+                     cstate.get_received_ping_frame_count());
+    return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
+                      "recv ping too frequent PING frame");
+  }
+
   // An endpoint MUST NOT respond to PING frames containing this flag.
   if (frame.header().flags & HTTP2_FLAGS_PING_ACK) {
     return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE);
@@ -713,7 +745,7 @@ rcv_window_update_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
   if (stream_id == 0) {
     // Connection level window update
     Http2StreamDebug(cstate.ua_session, stream_id, "Received WINDOW_UPDATE 
frame - updated to: %zd delta: %u",
-                     (cstate.client_rwnd + size), size);
+                     (cstate.client_rwnd() + size), size);
 
     // A sender MUST NOT allow a flow-control window to exceed 2^31-1
     // octets.  If a sender receives a WINDOW_UPDATE that causes a flow-
@@ -722,12 +754,16 @@ rcv_window_update_frame(Http2ConnectionState &cstate, 
const Http2Frame &frame)
     // sends a RST_STREAM with an error code of FLOW_CONTROL_ERROR; for the
     // connection, a GOAWAY frame with an error code of FLOW_CONTROL_ERROR
     // is sent.
-    if (size > HTTP2_MAX_WINDOW_SIZE - cstate.client_rwnd) {
+    if (size > HTTP2_MAX_WINDOW_SIZE - cstate.client_rwnd()) {
       return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_FLOW_CONTROL_ERROR,
                         "window update too big");
     }
 
-    cstate.client_rwnd += size;
+    auto error = cstate.increment_client_rwnd(size);
+    if (error != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
+      return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, error);
+    }
+
     cstate.restart_streams();
   } else {
     // Stream level window update
@@ -743,7 +779,7 @@ rcv_window_update_frame(Http2ConnectionState &cstate, const 
Http2Frame &frame)
     }
 
     Http2StreamDebug(cstate.ua_session, stream_id, "Received WINDOW_UPDATE 
frame - updated to: %zd delta: %u",
-                     (stream->client_rwnd + size), size);
+                     (stream->client_rwnd() + size), size);
 
     // A sender MUST NOT allow a flow-control window to exceed 2^31-1
     // octets.  If a sender receives a WINDOW_UPDATE that causes a flow-
@@ -752,14 +788,17 @@ rcv_window_update_frame(Http2ConnectionState &cstate, 
const Http2Frame &frame)
     // sends a RST_STREAM with an error code of FLOW_CONTROL_ERROR; for the
     // connection, a GOAWAY frame with an error code of FLOW_CONTROL_ERROR
     // is sent.
-    if (size > HTTP2_MAX_WINDOW_SIZE - stream->client_rwnd) {
+    if (size > HTTP2_MAX_WINDOW_SIZE - stream->client_rwnd()) {
       return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, 
Http2ErrorCode::HTTP2_ERROR_FLOW_CONTROL_ERROR,
                         "window update too big 2");
     }
 
-    stream->client_rwnd += size;
-    ssize_t wnd = std::min(cstate.client_rwnd, stream->client_rwnd);
+    auto error = stream->increment_client_rwnd(size);
+    if (error != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
+      return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, error);
+    }
 
+    ssize_t wnd = std::min(cstate.client_rwnd(), stream->client_rwnd());
     if (!stream->is_closed() && stream->get_state() == 
Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE && wnd > 0) {
       SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
       stream->restart_sending();
@@ -1158,7 +1197,7 @@ Http2ConnectionState::restart_streams()
     while (s != end) {
       Http2Stream *next = static_cast<Http2Stream *>(s->link.next ? 
s->link.next : stream_list.head);
       if (!s->is_closed() && s->get_state() == 
Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE &&
-          std::min(this->client_rwnd, s->client_rwnd) > 0) {
+          std::min(this->client_rwnd(), s->client_rwnd()) > 0) {
         SCOPED_MUTEX_LOCK(lock, s->mutex, this_ethread());
         s->restart_sending();
       }
@@ -1166,7 +1205,7 @@ Http2ConnectionState::restart_streams()
       s = next;
     }
     if (!s->is_closed() && s->get_state() == 
Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE &&
-        std::min(this->client_rwnd, s->client_rwnd) > 0) {
+        std::min(this->client_rwnd(), s->client_rwnd()) > 0) {
       SCOPED_MUTEX_LOCK(lock, s->mutex, this_ethread());
       s->restart_sending();
     }
@@ -1303,7 +1342,7 @@ Http2ConnectionState::update_initial_rwnd(Http2WindowSize 
new_size)
   // Update stream level window sizes
   for (Http2Stream *s = stream_list.head; s; s = static_cast<Http2Stream 
*>(s->link.next)) {
     SCOPED_MUTEX_LOCK(lock, s->mutex, this_ethread());
-    s->client_rwnd = new_size - 
(client_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) - s->client_rwnd);
+    s->update_initial_rwnd(new_size - 
(client_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) - s->client_rwnd()));
   }
 }
 
@@ -1332,7 +1371,7 @@ 
Http2ConnectionState::send_data_frames_depends_on_priority()
   Http2DependencyTree::Node *node = dependency_tree->top();
 
   // No node to send or no connection level window left
-  if (node == nullptr || client_rwnd <= 0) {
+  if (node == nullptr || _client_rwnd <= 0) {
     return;
   }
 
@@ -1374,7 +1413,7 @@ 
Http2ConnectionState::send_data_frames_depends_on_priority()
 Http2SendDataFrameResult
 Http2ConnectionState::send_a_data_frame(Http2Stream *stream, size_t 
&payload_length)
 {
-  const ssize_t window_size         = std::min(this->client_rwnd, 
stream->client_rwnd);
+  const ssize_t window_size         = std::min(this->client_rwnd(), 
stream->client_rwnd());
   const size_t buf_len              = 
BUFFER_SIZE_FOR_INDEX(buffer_size_index[HTTP2_FRAME_TYPE_DATA]);
   const size_t write_available_size = std::min(buf_len, 
static_cast<size_t>(window_size));
   size_t read_available_size        = 0;
@@ -1420,12 +1459,12 @@ Http2ConnectionState::send_a_data_frame(Http2Stream 
*stream, size_t &payload_len
   }
 
   // Update window size
-  this->client_rwnd -= payload_length;
-  stream->client_rwnd -= payload_length;
+  this->decrement_client_rwnd(payload_length);
+  stream->decrement_client_rwnd(payload_length);
 
   // Create frame
   Http2StreamDebug(ua_session, stream->get_id(), "Send a DATA frame - client 
window con: %5zd stream: %5zd payload: %5zd",
-                   client_rwnd, stream->client_rwnd, payload_length);
+                   _client_rwnd, stream->client_rwnd(), payload_length);
 
   Http2Frame data(HTTP2_FRAME_TYPE_DATA, stream->get_id(), flags);
   data.alloc(buffer_size_index[HTTP2_FRAME_TYPE_DATA]);
@@ -1859,6 +1898,72 @@ Http2ConnectionState::get_received_settings_count()
   return this->settings_count[0] + this->settings_count[1];
 }
 
+void
+Http2ConnectionState::increment_received_settings_frame_count()
+{
+  ink_hrtime hrtime_sec = Thread::get_hrtime() / HRTIME_SECOND;
+  uint8_t counter_index = ((hrtime_sec % 60) >= 30);
+
+  if ((hrtime_sec - 60) > this->settings_frame_count_last_update) {
+    this->settings_frame_count[0] = 0;
+    this->settings_frame_count[1] = 0;
+  } else if (counter_index != ((this->settings_frame_count_last_update % 60) 
>= 30)) {
+    this->settings_frame_count[counter_index] = 0;
+  }
+  ++this->settings_frame_count[counter_index];
+  this->settings_frame_count_last_update = hrtime_sec;
+}
+
+uint32_t
+Http2ConnectionState::get_received_settings_frame_count()
+{
+  return this->settings_frame_count[0] + this->settings_frame_count[1];
+}
+
+void
+Http2ConnectionState::increment_received_ping_frame_count()
+{
+  ink_hrtime hrtime_sec = Thread::get_hrtime() / HRTIME_SECOND;
+  uint8_t counter_index = ((hrtime_sec % 60) >= 30);
+
+  if ((hrtime_sec - 60) > this->ping_frame_count_last_update) {
+    this->ping_frame_count[0] = 0;
+    this->ping_frame_count[1] = 0;
+  } else if (counter_index != ((this->ping_frame_count_last_update % 60) >= 
30)) {
+    this->ping_frame_count[counter_index] = 0;
+  }
+  ++this->ping_frame_count[counter_index];
+  this->ping_frame_count_last_update = hrtime_sec;
+}
+
+uint32_t
+Http2ConnectionState::get_received_ping_frame_count()
+{
+  return this->ping_frame_count[0] + this->ping_frame_count[1];
+}
+
+void
+Http2ConnectionState::increment_received_priority_frame_count()
+{
+  ink_hrtime hrtime_sec = Thread::get_hrtime() / HRTIME_SECOND;
+  uint8_t counter_index = ((hrtime_sec % 60) >= 30);
+
+  if ((hrtime_sec - 60) > this->priority_frame_count_last_update) {
+    this->priority_frame_count[0] = 0;
+    this->priority_frame_count[1] = 0;
+  } else if (counter_index != ((this->priority_frame_count_last_update % 60) 
>= 30)) {
+    this->priority_frame_count[counter_index] = 0;
+  }
+  ++this->priority_frame_count[counter_index];
+  this->priority_frame_count_last_update = hrtime_sec;
+}
+
+uint32_t
+Http2ConnectionState::get_received_priority_frame_count()
+{
+  return this->priority_frame_count[0] + this->priority_frame_count[1];
+}
+
 // 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
@@ -1891,3 +1996,52 @@ Http2ConnectionState::_adjust_concurrent_stream()
 
   return Http2::max_concurrent_streams_in;
 }
+
+ssize_t
+Http2ConnectionState::client_rwnd() const
+{
+  return this->_client_rwnd;
+}
+
+Http2ErrorCode
+Http2ConnectionState::increment_client_rwnd(size_t amount)
+{
+  this->_client_rwnd += amount;
+
+  this->_recent_rwnd_increment[this->_recent_rwnd_increment_index] = amount;
+  ++this->_recent_rwnd_increment_index;
+  this->_recent_rwnd_increment_index %= this->_recent_rwnd_increment.size();
+  double sum = std::accumulate(this->_recent_rwnd_increment.begin(), 
this->_recent_rwnd_increment.end(), 0.0);
+  double avg = sum / this->_recent_rwnd_increment.size();
+  if (avg < Http2::min_avg_window_update) {
+    return Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM;
+  }
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
+
+Http2ErrorCode
+Http2ConnectionState::decrement_client_rwnd(size_t amount)
+{
+  this->_client_rwnd -= amount;
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
+
+ssize_t
+Http2ConnectionState::server_rwnd() const
+{
+  return this->_server_rwnd;
+}
+
+Http2ErrorCode
+Http2ConnectionState::increment_server_rwnd(size_t amount)
+{
+  this->_server_rwnd += amount;
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
+
+Http2ErrorCode
+Http2ConnectionState::decrement_server_rwnd(size_t amount)
+{
+  this->_server_rwnd -= amount;
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
diff --git a/proxy/http2/Http2ConnectionState.h 
b/proxy/http2/Http2ConnectionState.h
index 16dc1ae..b8177e8 100644
--- a/proxy/http2/Http2ConnectionState.h
+++ b/proxy/http2/Http2ConnectionState.h
@@ -238,10 +238,6 @@ public:
     return shutdown_reason;
   }
 
-  // Connection level window size
-  ssize_t client_rwnd = HTTP2_INITIAL_WINDOW_SIZE;
-  ssize_t server_rwnd = Http2::initial_window_size;
-
   // HTTP/2 frame sender
   void schedule_stream(Http2Stream *stream);
   void send_data_frames_depends_on_priority();
@@ -313,6 +309,19 @@ public:
 
   void increment_received_settings_count(uint32_t count);
   uint32_t get_received_settings_count();
+  void increment_received_settings_frame_count();
+  uint32_t get_received_settings_frame_count();
+  void increment_received_ping_frame_count();
+  uint32_t get_received_ping_frame_count();
+  void increment_received_priority_frame_count();
+  uint32_t get_received_priority_frame_count();
+
+  ssize_t client_rwnd() const;
+  Http2ErrorCode increment_client_rwnd(size_t amount);
+  Http2ErrorCode decrement_client_rwnd(size_t amount);
+  ssize_t server_rwnd() const;
+  Http2ErrorCode increment_server_rwnd(size_t amount);
+  Http2ErrorCode decrement_server_rwnd(size_t amount);
 
 private:
   unsigned _adjust_concurrent_stream();
@@ -340,10 +349,25 @@ private:
   // Counter for stream errors ATS sent
   uint32_t stream_error_count = 0;
 
+  // Connection level window size
+  ssize_t _client_rwnd = HTTP2_INITIAL_WINDOW_SIZE;
+  ssize_t _server_rwnd = Http2::initial_window_size;
+
+  std::vector<size_t> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, 
SIZE_MAX, SIZE_MAX};
+  int _recent_rwnd_increment_index           = 0;
+
   // Counter for settings received within last 60 seconds
   // Each item holds a count for 30 seconds.
   uint16_t settings_count[2]            = {0};
   ink_hrtime settings_count_last_update = 0;
+  // Counters for frames received within last 60 seconds
+  // Each item in an array holds a count for 30 seconds.
+  uint16_t settings_frame_count[2]            = {0};
+  ink_hrtime settings_frame_count_last_update = 0;
+  uint16_t ping_frame_count[2]                = {0};
+  ink_hrtime ping_frame_count_last_update     = 0;
+  uint16_t priority_frame_count[2]            = {0};
+  ink_hrtime priority_frame_count_last_update = 0;
 
   // NOTE: Id of stream which MUST receive CONTINUATION frame.
   //   - [RFC 7540] 6.2 HEADERS
diff --git a/proxy/http2/Http2Stream.cc b/proxy/http2/Http2Stream.cc
index 839e7f0..4a3bd34 100644
--- a/proxy/http2/Http2Stream.cc
+++ b/proxy/http2/Http2Stream.cc
@@ -26,6 +26,8 @@
 #include "Http2ClientSession.h"
 #include "../http/HttpSM.h"
 
+#include <numeric>
+
 #define Http2StreamDebug(fmt, ...) \
   SsnDebug(parent, "http2_stream", "[%" PRId64 "] [%u] " fmt, 
parent->connection_id(), this->get_id(), ##__VA_ARGS__);
 
@@ -881,6 +883,63 @@ Http2Stream::release(IOBufferReader *r)
   this->do_io_close();
 }
 
+ssize_t
+Http2Stream::client_rwnd() const
+{
+  return this->_client_rwnd;
+}
+
+Http2ErrorCode
+Http2Stream::increment_client_rwnd(size_t amount)
+{
+  this->_client_rwnd += amount;
+
+  this->_recent_rwnd_increment[this->_recent_rwnd_increment_index] = amount;
+  ++this->_recent_rwnd_increment_index;
+  this->_recent_rwnd_increment_index %= this->_recent_rwnd_increment.size();
+  double sum = std::accumulate(this->_recent_rwnd_increment.begin(), 
this->_recent_rwnd_increment.end(), 0.0);
+  double avg = sum / this->_recent_rwnd_increment.size();
+  if (avg < Http2::min_avg_window_update) {
+    return Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM;
+  }
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
+
+Http2ErrorCode
+Http2Stream::decrement_client_rwnd(size_t amount)
+{
+  this->_client_rwnd -= amount;
+  if (this->_client_rwnd < 0) {
+    return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR;
+  } else {
+    return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+  }
+}
+
+ssize_t
+Http2Stream::server_rwnd() const
+{
+  return this->_server_rwnd;
+}
+
+Http2ErrorCode
+Http2Stream::increment_server_rwnd(size_t amount)
+{
+  this->_server_rwnd += amount;
+  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+}
+
+Http2ErrorCode
+Http2Stream::decrement_server_rwnd(size_t amount)
+{
+  this->_server_rwnd -= amount;
+  if (this->_server_rwnd < 0) {
+    return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR;
+  } else {
+    return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
+  }
+}
+
 bool
 Http2Stream::_switch_thread_if_not_on_right_thread(int event, void *edata)
 {
diff --git a/proxy/http2/Http2Stream.h b/proxy/http2/Http2Stream.h
index 4f70a3d..96cebb0 100644
--- a/proxy/http2/Http2Stream.h
+++ b/proxy/http2/Http2Stream.h
@@ -38,7 +38,7 @@ class Http2Stream : public ProxyClientTransaction
 {
 public:
   typedef ProxyClientTransaction super; ///< Parent type.
-  Http2Stream(Http2StreamId sid = 0, ssize_t initial_rwnd = 
Http2::initial_window_size) : client_rwnd(initial_rwnd), _id(sid)
+  Http2Stream(Http2StreamId sid = 0, ssize_t initial_rwnd = 
Http2::initial_window_size) : _id(sid), _client_rwnd(initial_rwnd)
   {
     SET_HANDLER(&Http2Stream::main_event_handler);
   }
@@ -46,10 +46,10 @@ public:
   void
   init(Http2StreamId sid, ssize_t initial_rwnd)
   {
-    _id               = sid;
-    _start_time       = Thread::get_hrtime();
-    _thread           = this_ethread();
-    this->client_rwnd = initial_rwnd;
+    _id                = sid;
+    _start_time        = Thread::get_hrtime();
+    _thread            = this_ethread();
+    this->_client_rwnd = initial_rwnd;
     HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_CURRENT_CLIENT_STREAM_COUNT, 
_thread);
     HTTP2_INCREMENT_THREAD_DYN_STAT(HTTP2_STAT_TOTAL_CLIENT_STREAM_COUNT, 
_thread);
     sm_reader = request_reader = request_buffer.alloc_reader();
@@ -111,7 +111,7 @@ public:
   void
   update_initial_rwnd(Http2WindowSize new_size)
   {
-    client_rwnd = new_size;
+    this->_client_rwnd = new_size;
   }
 
   bool
@@ -165,8 +165,12 @@ public:
   void push_promise(URL &url, const MIMEField *accept_encoding);
 
   // Stream level window size
-  ssize_t client_rwnd;
-  ssize_t server_rwnd = Http2::initial_window_size;
+  ssize_t client_rwnd() const;
+  Http2ErrorCode increment_client_rwnd(size_t amount);
+  Http2ErrorCode decrement_client_rwnd(size_t amount);
+  ssize_t server_rwnd() const;
+  Http2ErrorCode increment_server_rwnd(size_t amount);
+  Http2ErrorCode decrement_server_rwnd(size_t amount);
 
   uint8_t *header_blocks        = nullptr;
   uint32_t header_blocks_length = 0;  // total length of header blocks (not 
include
@@ -283,6 +287,12 @@ private:
   uint64_t data_length = 0;
   uint64_t bytes_sent  = 0;
 
+  ssize_t _client_rwnd;
+  ssize_t _server_rwnd = Http2::initial_window_size;
+
+  std::vector<size_t> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, 
SIZE_MAX, SIZE_MAX};
+  int _recent_rwnd_increment_index           = 0;
+
   ChunkedHandler chunked_handler;
   Event *cross_thread_event      = nullptr;
   Event *buffer_full_write_event = nullptr;

Reply via email to