|
11 | 11 | #include "envoy/stats/scope.h"
|
12 | 12 |
|
13 | 13 | #include "common/common/assert.h"
|
| 14 | +#include "common/common/cleanup.h" |
14 | 15 | #include "common/common/enum_to_int.h"
|
15 | 16 | #include "common/common/fmt.h"
|
16 | 17 | #include "common/common/stack_array.h"
|
@@ -251,7 +252,13 @@ int ConnectionImpl::StreamImpl::onDataSourceSend(const uint8_t* framehd, size_t
|
251 | 252 | // https://nghttp2.org/documentation/types.html#c.nghttp2_send_data_callback
|
252 | 253 | static const uint64_t FRAME_HEADER_SIZE = 9;
|
253 | 254 |
|
254 |
| - Buffer::OwnedImpl output(framehd, FRAME_HEADER_SIZE); |
| 255 | + Buffer::OwnedImpl output; |
| 256 | + if (!parent_.addOutboundFrameFragment(output, framehd, FRAME_HEADER_SIZE)) { |
| 257 | + ENVOY_CONN_LOG(debug, "error sending data frame: Too many frames in the outbound queue", |
| 258 | + parent_.connection_); |
| 259 | + return NGHTTP2_ERR_FLOODED; |
| 260 | + } |
| 261 | + |
255 | 262 | output.move(pending_send_data_, length);
|
256 | 263 | parent_.connection_.write(output, false);
|
257 | 264 | return 0;
|
@@ -348,6 +355,10 @@ void ConnectionImpl::dispatch(Buffer::Instance& data) {
|
348 | 355 | dispatching_ = true;
|
349 | 356 | ssize_t rc =
|
350 | 357 | nghttp2_session_mem_recv(session_, static_cast<const uint8_t*>(slice.mem_), slice.len_);
|
| 358 | + if (rc == NGHTTP2_ERR_FLOODED) { |
| 359 | + throw FrameFloodException( |
| 360 | + "Flooding was detected in this HTTP/2 session, and it must be closed"); |
| 361 | + } |
351 | 362 | if (rc != static_cast<ssize_t>(slice.len_)) {
|
352 | 363 | throw CodecProtocolException(fmt::format("{}", nghttp2_strerror(rc)));
|
353 | 364 | }
|
@@ -555,9 +566,77 @@ int ConnectionImpl::onInvalidFrame(int32_t stream_id, int error_code) {
|
555 | 566 | return NGHTTP2_ERR_CALLBACK_FAILURE;
|
556 | 567 | }
|
557 | 568 |
|
| 569 | +int ConnectionImpl::onBeforeFrameSend(const nghttp2_frame* frame) { |
| 570 | + ENVOY_CONN_LOG(trace, "about to sent frame type={}, flags={}", connection_, |
| 571 | + static_cast<uint64_t>(frame->hd.type), static_cast<uint64_t>(frame->hd.flags)); |
| 572 | + ASSERT(!is_outbound_flood_monitored_control_frame_); |
| 573 | + // Flag flood monitored outbound control frames. |
| 574 | + is_outbound_flood_monitored_control_frame_ = |
| 575 | + ((frame->hd.type == NGHTTP2_PING || frame->hd.type == NGHTTP2_SETTINGS) && |
| 576 | + frame->hd.flags & NGHTTP2_FLAG_ACK) || |
| 577 | + frame->hd.type == NGHTTP2_RST_STREAM; |
| 578 | + return 0; |
| 579 | +} |
| 580 | + |
| 581 | +void ConnectionImpl::incrementOutboundFrameCount(bool is_outbound_flood_monitored_control_frame) { |
| 582 | + ++outbound_frames_; |
| 583 | + if (is_outbound_flood_monitored_control_frame) { |
| 584 | + ++outbound_control_frames_; |
| 585 | + } |
| 586 | + checkOutboundQueueLimits(); |
| 587 | +} |
| 588 | + |
| 589 | +bool ConnectionImpl::addOutboundFrameFragment(Buffer::OwnedImpl& output, const uint8_t* data, |
| 590 | + size_t length) { |
| 591 | + // Reset the outbound frame type (set in the onBeforeFrameSend callback) since the |
| 592 | + // onBeforeFrameSend callback is not called for DATA frames. |
| 593 | + bool is_outbound_flood_monitored_control_frame = false; |
| 594 | + std::swap(is_outbound_flood_monitored_control_frame, is_outbound_flood_monitored_control_frame_); |
| 595 | + try { |
| 596 | + incrementOutboundFrameCount(is_outbound_flood_monitored_control_frame); |
| 597 | + } catch (const FrameFloodException&) { |
| 598 | + return false; |
| 599 | + } |
| 600 | + |
| 601 | + auto fragment = Buffer::OwnedBufferFragmentImpl::create( |
| 602 | + absl::string_view(reinterpret_cast<const char*>(data), length), |
| 603 | + is_outbound_flood_monitored_control_frame ? control_frame_buffer_releasor_ |
| 604 | + : frame_buffer_releasor_); |
| 605 | + |
| 606 | + // The Buffer::OwnedBufferFragmentImpl object will be deleted in the *frame_buffer_releasor_ |
| 607 | + // callback. |
| 608 | + output.addBufferFragment(*fragment.release()); |
| 609 | + return true; |
| 610 | +} |
| 611 | + |
| 612 | +void ConnectionImpl::releaseOutboundFrame(const Buffer::OwnedBufferFragmentImpl* fragment) { |
| 613 | + ASSERT(outbound_frames_ >= 1); |
| 614 | + --outbound_frames_; |
| 615 | + delete fragment; |
| 616 | +} |
| 617 | + |
| 618 | +void ConnectionImpl::releaseOutboundControlFrame(const Buffer::OwnedBufferFragmentImpl* fragment) { |
| 619 | + ASSERT(outbound_control_frames_ >= 1); |
| 620 | + --outbound_control_frames_; |
| 621 | + releaseOutboundFrame(fragment); |
| 622 | +} |
| 623 | + |
558 | 624 | ssize_t ConnectionImpl::onSend(const uint8_t* data, size_t length) {
|
559 | 625 | ENVOY_CONN_LOG(trace, "send data: bytes={}", connection_, length);
|
560 |
| - Buffer::OwnedImpl buffer(data, length); |
| 626 | + Buffer::OwnedImpl buffer; |
| 627 | + if (!addOutboundFrameFragment(buffer, data, length)) { |
| 628 | + ENVOY_CONN_LOG(debug, "error sending frame: Too many frames in the outbound queue.", |
| 629 | + connection_); |
| 630 | + return NGHTTP2_ERR_FLOODED; |
| 631 | + } |
| 632 | + |
| 633 | + // While the buffer is transient the fragment it contains will be moved into the |
| 634 | + // write_buffer_ of the underlying connection_ by the write method below. |
| 635 | + // This creates lifetime dependency between the write_buffer_ of the underlying connection |
| 636 | + // and the codec object. Specifically the write_buffer_ MUST be either fully drained or |
| 637 | + // deleted before the codec object is deleted. This is presently guaranteed by the |
| 638 | + // destruction order of the Network::ConnectionImpl object where write_buffer_ is |
| 639 | + // destroyed before the filter_manager_ which owns the codec through Http::ConnectionManagerImpl. |
561 | 640 | connection_.write(buffer, false);
|
562 | 641 | return length;
|
563 | 642 | }
|
@@ -663,6 +742,15 @@ void ConnectionImpl::sendPendingFrames() {
|
663 | 742 | int rc = nghttp2_session_send(session_);
|
664 | 743 | if (rc != 0) {
|
665 | 744 | ASSERT(rc == NGHTTP2_ERR_CALLBACK_FAILURE);
|
| 745 | + // For errors caused by the pending outbound frame flood the FrameFloodException has |
| 746 | + // to be thrown. However the nghttp2 library returns only the generic error code for |
| 747 | + // all failure types. Check queue limits and throw FrameFloodException if they were |
| 748 | + // exceeded. |
| 749 | + if (outbound_frames_ > max_outbound_frames_ || |
| 750 | + outbound_control_frames_ > max_outbound_control_frames_) { |
| 751 | + throw FrameFloodException("Too many frames in the outbound queue."); |
| 752 | + } |
| 753 | + |
666 | 754 | throw CodecProtocolException(fmt::format("{}", nghttp2_strerror(rc)));
|
667 | 755 | }
|
668 | 756 |
|
@@ -810,6 +898,11 @@ ConnectionImpl::Http2Callbacks::Http2Callbacks() {
|
810 | 898 | return static_cast<ConnectionImpl*>(user_data)->onFrameSend(frame);
|
811 | 899 | });
|
812 | 900 |
|
| 901 | + nghttp2_session_callbacks_set_before_frame_send_callback( |
| 902 | + callbacks_, [](nghttp2_session*, const nghttp2_frame* frame, void* user_data) -> int { |
| 903 | + return static_cast<ConnectionImpl*>(user_data)->onBeforeFrameSend(frame); |
| 904 | + }); |
| 905 | + |
813 | 906 | nghttp2_session_callbacks_set_on_frame_not_send_callback(
|
814 | 907 | callbacks_, [](nghttp2_session*, const nghttp2_frame*, int, void*) -> int {
|
815 | 908 | // We used to always return failure here but it looks now this can get called if the other
|
@@ -979,6 +1072,31 @@ int ServerConnectionImpl::onHeader(const nghttp2_frame* frame, HeaderString&& na
|
979 | 1072 | return saveHeader(frame, std::move(name), std::move(value));
|
980 | 1073 | }
|
981 | 1074 |
|
| 1075 | +void ServerConnectionImpl::checkOutboundQueueLimits() { |
| 1076 | + if (outbound_frames_ > max_outbound_frames_ && dispatching_downstream_data_) { |
| 1077 | + stats_.outbound_flood_.inc(); |
| 1078 | + throw FrameFloodException("Too many frames in the outbound queue."); |
| 1079 | + } |
| 1080 | + if (outbound_control_frames_ > max_outbound_control_frames_ && dispatching_downstream_data_) { |
| 1081 | + stats_.outbound_control_flood_.inc(); |
| 1082 | + throw FrameFloodException("Too many control frames in the outbound queue."); |
| 1083 | + } |
| 1084 | +} |
| 1085 | + |
| 1086 | +void ServerConnectionImpl::dispatch(Buffer::Instance& data) { |
| 1087 | + ASSERT(!dispatching_downstream_data_); |
| 1088 | + dispatching_downstream_data_ = true; |
| 1089 | + |
| 1090 | + // Make sure the dispatching_downstream_data_ is set to false even |
| 1091 | + // when ConnectionImpl::dispatch throws an exception. |
| 1092 | + Cleanup cleanup([this]() { dispatching_downstream_data_ = false; }); |
| 1093 | + |
| 1094 | + // Make sure downstream outbound queue was not flooded by the upstream frames. |
| 1095 | + checkOutboundQueueLimits(); |
| 1096 | + |
| 1097 | + ConnectionImpl::dispatch(data); |
| 1098 | +} |
| 1099 | + |
982 | 1100 | } // namespace Http2
|
983 | 1101 | } // namespace Http
|
984 | 1102 | } // namespace Envoy
|
0 commit comments