diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 74fe641fe3a24..f47b9263765de 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -473,7 +473,7 @@ message KeepaliveSettings { [(validate.rules).duration = {gte {nanos: 1000000}}]; } -// [#next-free-field: 18] +// [#next-free-field: 19] message Http2ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http2ProtocolOptions"; @@ -662,6 +662,11 @@ message Http2ProtocolOptions { // Configure the maximum amount of metadata than can be handled per stream. Defaults to 1 MB. google.protobuf.UInt64Value max_metadata_size = 17; + + // Disables encoding the headers using huffman encoding. + // This can be useful in cases where the cpu spent encoding the headers isn't + // worth the network bandwidth saved e.g. for localhost. + bool disable_huffman = 18; } // [#not-implemented-hide:] diff --git a/bazel/foreign_cc/nghttp2_huffman.patch b/bazel/foreign_cc/nghttp2_huffman.patch new file mode 100644 index 0000000000000..320cbc2de1ce9 --- /dev/null +++ b/bazel/foreign_cc/nghttp2_huffman.patch @@ -0,0 +1,305 @@ +diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h +index 2ef49b8d..9bcd3ca9 100644 +--- a/lib/includes/nghttp2/nghttp2.h ++++ b/lib/includes/nghttp2/nghttp2.h +@@ -3156,6 +3156,15 @@ NGHTTP2_EXTERN void nghttp2_option_set_no_closed_streams(nghttp2_option *option, + NGHTTP2_EXTERN void nghttp2_option_set_max_outbound_ack(nghttp2_option *option, + size_t val); + ++/** ++ * @function ++ * ++ * This option sets whether nghttp2 will disable huffman encoding of headers ++ * to the receiver. ++*/ ++NGHTTP2_EXTERN void ++nghttp2_option_set_disable_huffman_encoding(nghttp2_option *option, int val); ++ + /** + * @function + * +diff --git a/lib/nghttp2_hd.c b/lib/nghttp2_hd.c +index 55fc2cc6..04f3d411 100644 +--- a/lib/nghttp2_hd.c ++++ b/lib/nghttp2_hd.c +@@ -719,10 +719,23 @@ int nghttp2_hd_deflate_init2(nghttp2_hd_deflater *deflater, + + deflater->deflate_hd_table_bufsize_max = max_deflate_dynamic_table_size; + deflater->min_hd_table_bufsize_max = UINT32_MAX; ++ deflater->disable_huffman = 0; + + return 0; + } + ++int nghttp2_hd_deflate_init3(nghttp2_hd_deflater *deflater, ++ size_t max_deflate_dynamic_table_size, ++ nghttp2_mem *mem) { ++ int rv = nghttp2_hd_deflate_init2( ++ deflater, NGHTTP2_HD_DEFAULT_MAX_DEFLATE_BUFFER_SIZE, mem); ++ if (rv == 0) { ++ deflater->disable_huffman = 1; ++ } ++ return rv; ++} ++ ++ + int nghttp2_hd_inflate_init(nghttp2_hd_inflater *inflater, nghttp2_mem *mem) { + int rv; + +@@ -1015,6 +1028,51 @@ static int emit_string(nghttp2_bufs *bufs, const uint8_t *str, size_t len) { + return rv; + } + ++/* ++ * Effectively the same as emit_string but with huffman pieces clobbered. ++ * ++ * While the code could be further hand optimized, the expectation is that ++ * the compiler will do constant propagation, dead code elimination, etc. ++ */ ++static int emit_string_nohuffman(nghttp2_bufs *bufs, const uint8_t *str, ++ size_t len) { ++ int rv; ++ uint8_t sb[16]; ++ uint8_t *bufp; ++ size_t blocklen; ++ const size_t enclen = len; ++ const int huffman = 0; ++ ++ blocklen = count_encoded_length(enclen, 7); ++ ++ DEBUGF("deflatehd: emit string str=%.*s, length=%zu, huffman=%d, " ++ "encoded_length=%zu\n", ++ (int)len, (const char *)str, len, huffman, enclen); ++ ++ if (sizeof(sb) < blocklen) { ++ return NGHTTP2_ERR_HEADER_COMP; ++ } ++ ++ bufp = sb; ++ *bufp = huffman ? 1 << 7 : 0; ++ encode_length(bufp, enclen, 7); ++ ++ rv = nghttp2_bufs_add(bufs, sb, blocklen); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ if (huffman) { ++ rv = nghttp2_hd_huff_encode(bufs, str, len); ++ } else { ++ assert(enclen == len); ++ rv = nghttp2_bufs_add(bufs, str, len); ++ } ++ ++ return rv; ++} ++ ++ + static uint8_t pack_first_byte(int indexing_mode) { + switch (indexing_mode) { + case NGHTTP2_HD_WITH_INDEXING: +@@ -1099,6 +1157,77 @@ static int emit_newname_block(nghttp2_bufs *bufs, const nghttp2_nv *nv, + return 0; + } + ++static int emit_indname_block_nohuffman(nghttp2_bufs *bufs, size_t idx, ++ const nghttp2_nv *nv, ++ int indexing_mode) { ++ int rv; ++ uint8_t *bufp; ++ size_t blocklen; ++ uint8_t sb[16]; ++ size_t prefixlen; ++ ++ if (indexing_mode == NGHTTP2_HD_WITH_INDEXING) { ++ prefixlen = 6; ++ } else { ++ prefixlen = 4; ++ } ++ ++ DEBUGF("deflatehd: emit indname index=%zu, valuelen=%zu, indexing_mode=%d\n", ++ idx, nv->valuelen, indexing_mode); ++ ++ blocklen = count_encoded_length(idx + 1, prefixlen); ++ ++ if (sizeof(sb) < blocklen) { ++ return NGHTTP2_ERR_HEADER_COMP; ++ } ++ ++ bufp = sb; ++ ++ *bufp = pack_first_byte(indexing_mode); ++ ++ encode_length(bufp, idx + 1, prefixlen); ++ ++ rv = nghttp2_bufs_add(bufs, sb, blocklen); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ rv = emit_string_nohuffman(bufs, nv->value, nv->valuelen); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ return 0; ++} ++ ++static int emit_newname_block_nohuffman(nghttp2_bufs *bufs, ++ const nghttp2_nv *nv, ++ int indexing_mode) { ++ int rv; ++ ++ DEBUGF( ++ "deflatehd: emit newname namelen=%zu, valuelen=%zu, indexing_mode=%d\n", ++ nv->namelen, nv->valuelen, indexing_mode); ++ ++ rv = nghttp2_bufs_addb(bufs, pack_first_byte(indexing_mode)); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ rv = emit_string_nohuffman(bufs, nv->name, nv->namelen); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ rv = emit_string_nohuffman(bufs, nv->value, nv->valuelen); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ return 0; ++} ++ ++ + static int add_hd_table_incremental(nghttp2_hd_context *context, + nghttp2_hd_nv *nv, nghttp2_hd_map *map, + uint32_t hash) { +@@ -1424,10 +1553,19 @@ static int deflate_nv(nghttp2_hd_deflater *deflater, nghttp2_bufs *bufs, + return NGHTTP2_ERR_HEADER_COMP; + } + } +- if (idx == -1) { +- rv = emit_newname_block(bufs, nv, indexing_mode); ++ ++ if (deflater->disable_huffman) { ++ if (idx == -1) { ++ rv = emit_newname_block_nohuffman(bufs, nv, indexing_mode); ++ } else { ++ rv = emit_indname_block_nohuffman(bufs, (size_t)idx, nv, indexing_mode); ++ } + } else { +- rv = emit_indname_block(bufs, (size_t)idx, nv, indexing_mode); ++ if (idx == -1) { ++ rv = emit_newname_block(bufs, nv, indexing_mode); ++ } else { ++ rv = emit_indname_block(bufs, (size_t)idx, nv, indexing_mode); ++ } + } + if (rv != 0) { + return rv; +diff --git a/lib/nghttp2_hd.h b/lib/nghttp2_hd.h +index 38a31a83..27b0f722 100644 +--- a/lib/nghttp2_hd.h ++++ b/lib/nghttp2_hd.h +@@ -227,6 +227,8 @@ struct nghttp2_hd_deflater { + /* If nonzero, send header table size using encoding context update + in the next deflate process */ + uint8_t notify_table_size_change; ++ /* Whether the deflater should not huffman encode header */ ++ uint8_t disable_huffman; + }; + + struct nghttp2_hd_inflater { +@@ -306,6 +308,16 @@ int nghttp2_hd_deflate_init2(nghttp2_hd_deflater *deflater, + size_t max_deflate_dynamic_table_size, + nghttp2_mem *mem); + ++/* ++ * Initializes |deflater| for deflating name/values pairs. ++ * ++ * This is `nghttp2_hd_deflate_init2` with the addition of tracking that ++ * huffman encoding should be disabled for this deflater. ++ */ ++int nghttp2_hd_deflate_init3(nghttp2_hd_deflater *deflater, ++ size_t max_deflate_dynamic_table_size, ++ nghttp2_mem *mem); ++ + /* + * Deallocates any resources allocated for |deflater|. + */ +diff --git a/lib/nghttp2_option.c b/lib/nghttp2_option.c +index 02a24eee..38ed503e 100644 +--- a/lib/nghttp2_option.c ++++ b/lib/nghttp2_option.c +@@ -116,6 +116,12 @@ void nghttp2_option_set_max_deflate_dynamic_table_size(nghttp2_option *option, + option->max_deflate_dynamic_table_size = val; + } + ++void nghttp2_option_set_disable_huffman_encoding(nghttp2_option *option, ++ int val) { ++ option->opt_set_mask |= NGHTTP2_OPT_DISABLE_HUFFMAN; ++ option->disable_huffman = val; ++} ++ + void nghttp2_option_set_no_closed_streams(nghttp2_option *option, int val) { + option->opt_set_mask |= NGHTTP2_OPT_NO_CLOSED_STREAMS; + option->no_closed_streams = val; +diff --git a/lib/nghttp2_option.h b/lib/nghttp2_option.h +index c89cb97f..74141d89 100644 +--- a/lib/nghttp2_option.h ++++ b/lib/nghttp2_option.h +@@ -72,6 +72,7 @@ typedef enum { + NGHTTP2_OPT_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION = 1 << 14, + NGHTTP2_OPT_STREAM_RESET_RATE_LIMIT = 1 << 15, + NGHTTP2_OPT_MAX_CONTINUATIONS = 1 << 16, ++ NGHTTP2_OPT_DISABLE_HUFFMAN = 1 << 30, + } nghttp2_option_flag; + + /** +@@ -91,6 +92,10 @@ struct nghttp2_option { + * NGHTTP2_OPT_MAX_DEFLATE_DYNAMIC_TABLE_SIZE + */ + size_t max_deflate_dynamic_table_size; ++ /** ++ * NGHTTP2_OPT_DISABLE_HUFFMAN ++ */ ++ int disable_huffman; + /** + * NGHTTP2_OPT_MAX_OUTBOUND_ACK + */ +diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c +index df33a89e..48e17f6c 100644 +--- a/lib/nghttp2_session.c ++++ b/lib/nghttp2_session.c +@@ -441,6 +441,7 @@ static int session_new(nghttp2_session **session_ptr, + size_t max_deflate_dynamic_table_size = + NGHTTP2_HD_DEFAULT_MAX_DEFLATE_BUFFER_SIZE; + size_t i; ++ int deflater_inited = 0; + + if (mem == NULL) { + mem = nghttp2_mem_default(); +@@ -585,10 +586,19 @@ static int session_new(nghttp2_session **session_ptr, + if (option->opt_set_mask & NGHTTP2_OPT_MAX_CONTINUATIONS) { + (*session_ptr)->max_continuations = option->max_continuations; + } ++ ++ if (option->opt_set_mask & NGHTTP2_OPT_DISABLE_HUFFMAN && option->disable_huffman) { ++ rv = nghttp2_hd_deflate_init3(&(*session_ptr)->hd_deflater, ++ max_deflate_dynamic_table_size, mem); ++ deflater_inited = 1; ++ } ++ } ++ ++ if (!deflater_inited) { ++ rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater, ++ max_deflate_dynamic_table_size, mem); + } + +- rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater, +- max_deflate_dynamic_table_size, mem); + if (rv != 0) { + goto fail_hd_deflater; + } diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index 965a82a182e76..13152343f7300 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -558,7 +558,10 @@ def _com_github_nghttp2_nghttp2(): # This patch cannot be picked up due to ABI rules. Discussion at; # https://github.com/nghttp2/nghttp2/pull/1395 # https://github.com/envoyproxy/envoy/pull/8572#discussion_r334067786 - patches = ["@envoy//bazel/foreign_cc:nghttp2.patch"], + patches = [ + "@envoy//bazel/foreign_cc:nghttp2.patch", + "@envoy//bazel/foreign_cc:nghttp2_huffman.patch", + ], ) native.bind( name = "nghttp2", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index c0457accb830a..f28bb4d903056 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -236,6 +236,11 @@ new_features: - area: http2 change: | Added new parameter to the ``sendGoAwayAndClose`` to support gracefully closing of HTTP/2 connection. +- area: http2 + change: | + Added :ref:`disable_huffman ` disable huffman + encoding of headers from envoy to the receiver. This is useful in scenarios where the bandwidth saved from huffman + encoding is not worth the cpu cost. e.g. for localhost, sidecar traffic. - area: logging change: | Added support for the not-equal operator in access log filter rules, in diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index f13b1b0217137..75fd4034ed965 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -2037,6 +2037,13 @@ ConnectionImpl::Http2Options::Http2Options( og_options_.max_header_field_size = max_headers_kb * 1024; og_options_.allow_extended_connect = http2_options.allow_connect(); og_options_.allow_different_host_and_authority = true; + if (http2_options.disable_huffman()) { + if (http2_options.has_hpack_table_size() && http2_options.hpack_table_size().value() == 0) { + og_options_.compression_option = http2::adapter::OgHttp2Session::Options::DISABLE_HUFFMAN; + } else { + og_options_.compression_option = http2::adapter::OgHttp2Session::Options::DISABLE_COMPRESSION; + } + } #ifdef ENVOY_ENABLE_UHV // UHV - disable header validations in oghttp2 @@ -2067,6 +2074,10 @@ ConnectionImpl::Http2Options::Http2Options( http2_options.hpack_table_size().value()); } + if (http2_options.disable_huffman()) { + nghttp2_option_set_disable_huffman_encoding(options_, 1); + } + if (http2_options.allow_metadata()) { nghttp2_option_set_user_recv_extension_type(options_, METADATA_FRAME_TYPE); } else { diff --git a/test/common/http/http2/codec_impl_test.cc b/test/common/http/http2/codec_impl_test.cc index a8f7216baae93..902e924b8da17 100644 --- a/test/common/http/http2/codec_impl_test.cc +++ b/test/common/http/http2/codec_impl_test.cc @@ -1,4 +1,5 @@ #include +#include #include #include @@ -3200,6 +3201,99 @@ TEST_P(Http2CodecImplTestAll, TestCodecHeaderCompression) { } } +TEST_P(Http2CodecImplTest, TestCanDisableHuffmanEncoding) { + TestRequestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + request_headers.addCopy("x-well-compressable-header", std::string(1000, 'a')); + + // Create a connection with huffman disabled. + client_http2_options_.set_disable_huffman(true); + initialize(); + + std::string buffer_without_huffman; + ON_CALL(client_connection_, write(_, _)) + .WillByDefault(Invoke([&buffer_without_huffman, this](Buffer::Instance& data, bool) -> void { + buffer_without_huffman.append(data.toString()); + server_wrapper_->buffer_.add(data); + })); + + EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)); + EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok()); + driveToCompletion(); + + ASSERT_EQ(client_wrapper_->buffer_.length(), 0); + ASSERT_EQ(server_wrapper_->buffer_.length(), 0); + + // Create a connection with huffman enabled. + client_http2_options_.set_disable_huffman(false); + NiceMock client_connection2; + MockConnectionCallbacks client_callbacks2; + client_ = std::make_unique( + client_connection2, client_callbacks2, *client_stats_store_.rootScope(), + client_http2_options_, random_, max_request_headers_kb_, max_response_headers_count_, + ProdNghttp2SessionFactory::get()); + client_wrapper_ = std::make_unique(client_.get()); + + NiceMock server_connection2; + MockServerConnectionCallbacks server_callbacks2; + + server_ = std::make_unique( + server_connection2, server_callbacks2, *server_stats_store_.rootScope(), + server_http2_options_, random_, max_request_headers_kb_, max_request_headers_count_, + headers_with_underscores_action_); + server_wrapper_ = std::make_unique(server_.get()); + + // Setup connection mocks for the second connection + ON_CALL(server_connection2, write(_, _)) + .WillByDefault(Invoke( + [this](Buffer::Instance& data, bool) -> void { client_wrapper_->buffer_.add(data); })); + ON_CALL(client_connection2, write(_, _)) + .WillByDefault(Invoke( + [this](Buffer::Instance& data, bool) -> void { server_wrapper_->buffer_.add(data); })); + + driveToCompletion(); + + // Set up stream for the second connection + MockResponseDecoder response_decoder2; + auto request_encoder2 = &client_->newStream(response_decoder2); + ResponseEncoder* response_encoder2 = nullptr; + MockStreamCallbacks server_stream_callbacks2; + MockCodecEventCallbacks server_codec_event_callbacks2; + MockRequestDecoder request_decoder2; + + EXPECT_CALL(server_callbacks2, newStream(_, _)) + .WillOnce(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& { + response_encoder2 = &encoder; + encoder.getStream().addCallbacks(server_stream_callbacks2); + encoder.getStream().registerCodecEventCallbacks(&server_codec_event_callbacks2); + encoder.getStream().setFlushTimeout(std::chrono::milliseconds(30000)); + return request_decoder2; + })); + + // Capture the header frame encoded. + std::string buffer_with_huffman; + ON_CALL(client_connection2, write(_, _)) + .WillByDefault(Invoke([this, &buffer_with_huffman](Buffer::Instance& data, bool) -> void { + server_wrapper_->buffer_.add(data); + buffer_with_huffman.append(data.toString()); + })); + + // Encode headers with Huffman encoding + EXPECT_CALL(request_decoder2, decodeHeaders_(_, true)); + EXPECT_TRUE(request_encoder2->encodeHeaders(request_headers, true).ok()); + + // Drive to completion + driveToCompletion(); + + // Verify that the two encoded buffers are different + EXPECT_NE(buffer_without_huffman, buffer_with_huffman); + + // Huffman encoding is smaller with these particular headers. + EXPECT_GT(buffer_without_huffman.length(), 0); + EXPECT_GT(buffer_with_huffman.length(), 0); + EXPECT_GT(buffer_without_huffman.length(), buffer_with_huffman.length()); +} + // Verify that codec detects PING flood TEST_P(Http2CodecImplTest, PingFlood) { initialize();