Skip to content

Commit

Permalink
Merge pull request #3291 from h2o/kazuho/h2-rapid-reset
Browse files Browse the repository at this point in the history
[http2] rapid reset attack
  • Loading branch information
kazuho committed Oct 10, 2023
2 parents cb9f500 + 94fbc54 commit 28fe151
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 5 deletions.
4 changes: 4 additions & 0 deletions include/h2o.h
Expand Up @@ -431,6 +431,10 @@ struct st_h2o_globalconf_t {
h2o_socket_latency_optimization_conditions_t latency_optimization;
/* */
h2o_iovec_t origin_frame;
/**
* milliseconds to delay processing requests when suspicious behavior is detected
*/
uint64_t dos_delay;
} http2;

struct {
Expand Down
8 changes: 8 additions & 0 deletions include/h2o/http2_internal.h
Expand Up @@ -113,6 +113,7 @@ struct st_h2o_http2_stream_t {
} pull;
};
unsigned blocked_by_server : 1;
unsigned reset_by_peer : 1;
/**
* state of the ostream, only used in push mode
*/
Expand Down Expand Up @@ -225,6 +226,13 @@ struct st_h2o_http2_conn_t {
* timeout entry used for graceful shutdown
*/
h2o_timer_t _graceful_shutdown_timeout;
/**
* DoS mitigation; the idea here is to delay processing requests when observing suspicious behavior
*/
struct {
h2o_timer_t process_delay;
size_t reset_budget; /* RST_STREAM frames are considered suspicious when this value goes down to zero */
} dos_mitigation;
};

/* connection */
Expand Down
1 change: 1 addition & 0 deletions lib/core/config.c
Expand Up @@ -200,6 +200,7 @@ void h2o_config_init(h2o_globalconf_t *config)
config->http2.latency_optimization.min_rtt = 50; // milliseconds
config->http2.latency_optimization.max_additional_delay = 10;
config->http2.latency_optimization.max_cwnd = 65535;
config->http2.dos_delay = 100; /* 100ms processing delay when observing suspicious behavior */
config->http3.idle_timeout = quicly_spec_context.transport_params.max_idle_timeout;
config->http3.active_stream_window_size = H2O_DEFAULT_HTTP3_ACTIVE_STREAM_WINDOW_SIZE;
config->http3.allow_delayed_ack = 1;
Expand Down
8 changes: 8 additions & 0 deletions lib/core/configurator.c
Expand Up @@ -575,6 +575,11 @@ static int on_config_http2_casper(h2o_configurator_command_t *cmd, h2o_configura
return 0;
}

static int on_config_http2_dos_delay(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
return config_timeout(cmd, node, &ctx->globalconf->http2.dos_delay);
}

static int on_config_http3_idle_timeout(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
return config_timeout(cmd, node, &ctx->globalconf->http3.idle_timeout);
Expand Down Expand Up @@ -1082,6 +1087,9 @@ void h2o_configurator__init_core(h2o_globalconf_t *conf)
on_config_http2_allow_cross_origin_push);
h2o_configurator_define_command(&c->super, "http2-casper", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_HOST,
on_config_http2_casper);
h2o_configurator_define_command(&c->super, "http2-dos-delay",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_http2_dos_delay);
h2o_configurator_define_command(&c->super, "http3-idle-timeout",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_http3_idle_timeout);
Expand Down
44 changes: 39 additions & 5 deletions lib/http2/connection.c
Expand Up @@ -201,6 +201,9 @@ static void process_request(h2o_http2_conn_t *conn, h2o_http2_stream_t *stream)

static void run_pending_requests(h2o_http2_conn_t *conn)
{
if (h2o_timer_is_linked(&conn->dos_mitigation.process_delay))
return;

h2o_linklist_t *link, *lnext;
int ran_one_request;

Expand Down Expand Up @@ -328,6 +331,16 @@ void h2o_http2_conn_unregister_stream(h2o_http2_conn_t *conn, h2o_http2_stream_t
if (stream->blocked_by_server)
h2o_http2_stream_set_blocked_by_server(conn, stream, 0);

/* Decrement reset_budget if the stream was reset by peer, otherwise increment. By doing so, we penalize connections that
* generate resets for >50% of requests. */
if (stream->reset_by_peer) {
if (conn->dos_mitigation.reset_budget > 0)
--conn->dos_mitigation.reset_budget;
} else {
if (conn->dos_mitigation.reset_budget < conn->super.ctx->globalconf->http2.max_concurrent_requests_per_connection)
++conn->dos_mitigation.reset_budget;
}

switch (stream->state) {
case H2O_HTTP2_STREAM_STATE_RECV_BODY:
if (h2o_linklist_is_linked(&stream->_link))
Expand Down Expand Up @@ -388,6 +401,9 @@ void close_connection_now(h2o_http2_conn_t *conn)
if (h2o_timer_is_linked(&conn->_graceful_shutdown_timeout))
h2o_timer_unlink(&conn->_graceful_shutdown_timeout);

if (h2o_timer_is_linked(&conn->dos_mitigation.process_delay))
h2o_timer_unlink(&conn->dos_mitigation.process_delay);

h2o_buffer_dispose(&conn->_write.buf);
if (conn->_write.buf_in_flight != NULL)
h2o_buffer_dispose(&conn->_write.buf_in_flight);
Expand Down Expand Up @@ -1196,11 +1212,19 @@ static int handle_rst_stream_frame(h2o_http2_conn_t *conn, h2o_http2_frame_t *fr
return H2O_HTTP2_ERROR_PROTOCOL;
}

stream = h2o_http2_conn_get_stream(conn, frame->stream_id);
if (stream != NULL) {
/* reset the stream */
h2o_http2_stream_reset(conn, stream);
}
if ((stream = h2o_http2_conn_get_stream(conn, frame->stream_id)) == NULL)
return 0;

/* reset the stream */
stream->reset_by_peer = 1;
h2o_http2_stream_reset(conn, stream);

/* setup process delay if we've just ran out of reset budget */
if (conn->dos_mitigation.reset_budget == 0 && conn->super.ctx->globalconf->http2.dos_delay != 0 &&
!h2o_timer_is_linked(&conn->dos_mitigation.process_delay))
h2o_timer_link(conn->super.ctx->loop, conn->super.ctx->globalconf->http2.dos_delay,
&conn->dos_mitigation.process_delay);

/* TODO log */

return 0;
Expand Down Expand Up @@ -1685,6 +1709,14 @@ static h2o_iovec_t log_priority_actual_weight(h2o_req_t *req)
return h2o_iovec_init(s, len);
}

static void on_dos_process_delay(h2o_timer_t *timer)
{
h2o_http2_conn_t *conn = H2O_STRUCT_FROM_MEMBER(h2o_http2_conn_t, dos_mitigation.process_delay, timer);

assert(!h2o_timer_is_linked(&conn->dos_mitigation.process_delay));
run_pending_requests(conn);
}

static h2o_http2_conn_t *create_conn(h2o_context_t *ctx, h2o_hostconf_t **hosts, h2o_socket_t *sock, struct timeval connected_at)
{
static const h2o_conn_callbacks_t callbacks = {
Expand Down Expand Up @@ -1755,6 +1787,8 @@ static h2o_http2_conn_t *create_conn(h2o_context_t *ctx, h2o_hostconf_t **hosts,
h2o_linklist_init_anchor(&conn->early_data.blocked_streams);
conn->is_chromium_dependency_tree = 1; /* initially assume the client is Chromium until proven otherwise */
conn->received_any_request = 0;
conn->dos_mitigation.process_delay.cb = on_dos_process_delay;
conn->dos_mitigation.reset_budget = conn->super.ctx->globalconf->http2.max_concurrent_requests_per_connection;

return conn;
}
Expand Down

0 comments on commit 28fe151

Please sign in to comment.