Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2
api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"//envoy/extensions/filters/network/http_connection_manager/v3:pkg",
"@xds//udpa/annotations:pkg",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";

package envoy.extensions.filters.network.reverse_tunnel.v3;

import "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto";

import "udpa/annotations/status.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.network.reverse_tunnel.v3";
option java_outer_classname = "DrainAwareHcmProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/reverse_tunnel/v3;reverse_tunnelv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Drain-Aware HTTP Connection Manager]
// A network filter that wraps the standard HCM and sends HTTP/2 GOAWAY frames
// on all active connections when the listener is drained (e.g. via admin
// /drain_listeners). Intended for use on reverse-tunnel listeners so that
// agents can detect drain and re-establish connections gracefully.
// [#extension: envoy.filters.network.reverse_tunnel_drain_aware_http_connection_manager]

// Configuration for the drain-aware HTTP Connection Manager.
// All HCM fields are forwarded verbatim; the only difference is that this
// filter registers a listener-drain callback and emits a GOAWAY before the
// connection is closed.
message DrainAwareHttpConnectionManager {
// The underlying HCM configuration to apply.
http_connection_manager.v3.HttpConnectionManager hcm_config = 1;
}
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ EXTENSIONS = {
"envoy.filters.network.ext_authz": "//source/extensions/filters/network/ext_authz:config",
"envoy.filters.network.ext_proc": "//source/extensions/filters/network/ext_proc:config",
"envoy.filters.network.reverse_tunnel": "//source/extensions/filters/network/reverse_tunnel:config",
"envoy.filters.network.reverse_tunnel_drain_aware_http_connection_manager": "//source/extensions/filters/network/reverse_tunnel/drain_aware_hcm:drain_aware_config",
"envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config",
"envoy.filters.network.local_ratelimit": "//source/extensions/filters/network/local_ratelimit:config",
"envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config",
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,13 @@ envoy.filters.network.reverse_tunnel:
status: alpha
type_urls:
- envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel
envoy.filters.network.reverse_tunnel_drain_aware_http_connection_manager:
categories:
- envoy.filters.network
security_posture: unknown
status: wip
type_urls:
- envoy.extensions.filters.network.reverse_tunnel.v3.DrainAwareHttpConnectionManager
envoy.filters.network.http_connection_manager:
categories:
- envoy.filters.network
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_extension(
name = "drain_aware_config",
srcs = ["drain_aware_config.cc"],
hdrs = [
"drain_aware_config.h",
"drain_aware_server_connection.h",
],
deps = [
"//envoy/common:exception_lib",
"//envoy/event:dispatcher_interface",
"//envoy/http:codec_interface",
"//envoy/network:drain_decision_interface",
"//envoy/server:factory_context_interface",
"//source/common/http:conn_manager_lib",
"//source/extensions/filters/network:well_known_names",
"//source/extensions/filters/network/common:factory_base_lib",
"//source/extensions/filters/network/http_connection_manager:config",
"@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include "source/extensions/filters/network/reverse_tunnel/drain_aware_hcm/drain_aware_config.h"

#include "envoy/common/exception.h"

#include "source/common/common/logger.h"
#include "source/extensions/filters/network/reverse_tunnel/drain_aware_hcm/drain_aware_server_connection.h"

namespace Envoy {
namespace Extensions {
namespace NetworkFilters {
namespace ReverseTunnel {

Http::ServerConnectionPtr DrainAwareHttpConnectionManagerConfig::createBaseCodec(
Network::Connection& connection, const Buffer::Instance& data,
Http::ServerConnectionCallbacks& callbacks, Server::OverloadManager& overload_manager) {
return HttpConnectionManager::HttpConnectionManagerConfig::createCodec(
connection, data, callbacks, overload_manager);
}

Http::ServerConnectionPtr DrainAwareHttpConnectionManagerConfig::createCodec(
Network::Connection& connection, const Buffer::Instance& data,
Http::ServerConnectionCallbacks& callbacks, Server::OverloadManager& overload_manager) {
auto codec = createBaseCodec(connection, data, callbacks, overload_manager);
if (codec == nullptr) {
ENVOY_LOG_MISC(warn, "drain_aware_hcm: base createCodec returned nullptr for connection id={}",
connection.id());
return codec;
}

// Use the listener-level DrainDecision, NOT serverFactoryContext().drainManager().
// /drain_listeners fires the listener-level drain decision; the server-wide drain manager
// only fires on hot-restart / full server shutdown.
return std::make_unique<DrainAwareServerConnection>(std::move(codec), connection.dispatcher(),
factory_context_.drainDecision());
}

absl::StatusOr<Network::FilterFactoryCb>
DrainAwareHttpConnectionManagerFilterConfigFactory::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::network::reverse_tunnel::v3::DrainAwareHttpConnectionManager&
proto_config,
Server::Configuration::FactoryContext& context) {
const auto& hcm_config = proto_config.hcm_config();
auto singletons = HttpConnectionManager::Utility::createSingletons(context);

absl::Status creation_status = absl::OkStatus();
auto filter_config = std::make_shared<DrainAwareHttpConnectionManagerConfig>(
hcm_config, context, *singletons.date_provider_, *singletons.route_config_provider_manager_,
singletons.scoped_routes_config_provider_manager_.get(), *singletons.tracer_manager_,
*singletons.filter_config_provider_manager_, creation_status);
RETURN_IF_NOT_OK(creation_status);

return [singletons, filter_config, &context](Network::FilterManager& filter_manager) -> void {
auto& server_context = context.serverFactoryContext();
Server::OverloadManager& overload_manager = context.listenerInfo().shouldBypassOverloadManager()
? server_context.nullOverloadManager()
: server_context.overloadManager();
auto hcm = std::make_shared<Http::ConnectionManagerImpl>(
filter_config, context.drainDecision(), server_context.api().randomGenerator(),
server_context.httpContext(), server_context.runtime(), server_context.localInfo(),
server_context.clusterManager(), overload_manager,
server_context.mainThreadDispatcher().timeSource(), context.listenerInfo().direction());
filter_manager.addReadFilter(std::move(hcm));
};
}

REGISTER_FACTORY(DrainAwareHttpConnectionManagerFilterConfigFactory,
Server::Configuration::NamedNetworkFilterConfigFactory);

} // namespace ReverseTunnel
} // namespace NetworkFilters
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once

#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"
#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h"
#include "envoy/extensions/filters/network/reverse_tunnel/v3/drain_aware_hcm.pb.h"
#include "envoy/extensions/filters/network/reverse_tunnel/v3/drain_aware_hcm.pb.validate.h"

#include "source/extensions/filters/network/common/factory_base.h"
#include "source/extensions/filters/network/http_connection_manager/config.h"
#include "source/extensions/filters/network/well_known_names.h"

namespace Envoy {
namespace Extensions {
namespace NetworkFilters {
namespace ReverseTunnel {

class DrainAwareHttpConnectionManagerConfig
: public HttpConnectionManager::HttpConnectionManagerConfig {
public:
DrainAwareHttpConnectionManagerConfig(
const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
config,
Server::Configuration::FactoryContext& context, Http::DateProvider& date_provider,
Router::RouteConfigProviderManager& route_config_provider_manager,
Config::ConfigProviderManager* scoped_routes_config_provider_manager,
Tracing::TracerManager& tracer_manager,
HttpConnectionManager::FilterConfigProviderManager& filter_config_provider_manager,
absl::Status& creation_status)
: HttpConnectionManager::HttpConnectionManagerConfig(
config, context, date_provider, route_config_provider_manager,
scoped_routes_config_provider_manager, tracer_manager, filter_config_provider_manager,
creation_status),
factory_context_(context) {}

Http::ServerConnectionPtr createCodec(Network::Connection& connection,
const Buffer::Instance& data,
Http::ServerConnectionCallbacks& callbacks,
Server::OverloadManager& overload_manager) override;

protected:
// Virtual seam for testing: allows tests to override the base codec creation
// to exercise the nullptr defensive check in createCodec().
virtual Http::ServerConnectionPtr createBaseCodec(Network::Connection& connection,
const Buffer::Instance& data,
Http::ServerConnectionCallbacks& callbacks,
Server::OverloadManager& overload_manager);

private:
Server::Configuration::FactoryContext& factory_context_;
};

class DrainAwareHttpConnectionManagerFilterConfigFactory
: public Common::ExceptionFreeFactoryBase<envoy::extensions::filters::network::reverse_tunnel::
v3::DrainAwareHttpConnectionManager> {
public:
DrainAwareHttpConnectionManagerFilterConfigFactory()
: ExceptionFreeFactoryBase(NetworkFilterNames::get().ReverseTunnelDrainAwareHcm, true) {}

private:
absl::StatusOr<Network::FilterFactoryCb>
createFilterFactoryFromProtoTyped(const envoy::extensions::filters::network::reverse_tunnel::v3::
DrainAwareHttpConnectionManager& proto_config,
Server::Configuration::FactoryContext& context) override;
};

} // namespace ReverseTunnel
} // namespace NetworkFilters
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#pragma once

#include <memory>

#include "envoy/event/dispatcher.h"
#include "envoy/http/codec.h"
#include "envoy/network/drain_decision.h"

#include "source/common/common/logger.h"

namespace Envoy {
namespace Extensions {
namespace NetworkFilters {
namespace ReverseTunnel {

// Wraps an Http::ServerConnection and proactively sends an HTTP/2 GOAWAY frame when
// the listener that owns this connection begins draining. Drain is detected by polling
// DrainDecision::drainClose() on a short timer; this avoids calling addOnDrainCloseCb()
// which is intentionally unsupported on PerFilterChainFactoryContextImpl.
class DrainAwareServerConnection : public Http::ServerConnection,
public Logger::Loggable<Logger::Id::filter> {
public:
DrainAwareServerConnection(Http::ServerConnectionPtr inner, Event::Dispatcher& dispatcher,
const Network::DrainDecision& drain_decision)
: inner_(std::move(inner)), drain_decision_(drain_decision) {
ENVOY_LOG(debug, "drain_aware_hcm: created server connection wrapper, protocol={}",
static_cast<int>(inner_->protocol()));
drain_check_timer_ = dispatcher.createTimer([this]() { onDrainCheckTimer(); });
drain_check_timer_->enableTimer(std::chrono::milliseconds(100));
}

~DrainAwareServerConnection() override {
if (drain_check_timer_ != nullptr) {
drain_check_timer_->disableTimer();
}
}

Http::Status dispatch(Buffer::Instance& data) override { return inner_->dispatch(data); }
void goAway() override { inner_->goAway(); }
Http::Protocol protocol() override { return inner_->protocol(); }
void shutdownNotice() override { inner_->shutdownNotice(); }
bool wantsToWrite() override { return inner_->wantsToWrite(); }

void onUnderlyingConnectionAboveWriteBufferHighWatermark() override {
inner_->onUnderlyingConnectionAboveWriteBufferHighWatermark();
}

void onUnderlyingConnectionBelowWriteBufferLowWatermark() override {
inner_->onUnderlyingConnectionBelowWriteBufferLowWatermark();
}

private:
void onDrainCheckTimer() {
if (drain_goaway_sent_) {
return;
}
if (drain_decision_.drainClose(Network::DrainDirection::All)) {
ENVOY_LOG(info, "drain_aware_hcm: drain detected, sending GOAWAY");
drain_goaway_sent_ = true;
inner_->goAway();
return;
}
drain_check_timer_->enableTimer(std::chrono::milliseconds(100));
}

Http::ServerConnectionPtr inner_;
const Network::DrainDecision& drain_decision_;
Event::TimerPtr drain_check_timer_;
bool drain_goaway_sent_{false};
};

} // namespace ReverseTunnel
} // namespace NetworkFilters
} // namespace Extensions
} // namespace Envoy
3 changes: 3 additions & 0 deletions source/extensions/filters/network/well_known_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class NetworkFilterNameValues {
const std::string NetworkMatchDelegate = "envoy.filters.network.match_delegate";
// Reverse tunnel filter
const std::string ReverseTunnel = "envoy.filters.network.reverse_tunnel";
// Reverse tunnel-specific drain-aware HTTP connection manager filter
const std::string ReverseTunnelDrainAwareHcm =
"envoy.filters.network.reverse_tunnel_drain_aware_http_connection_manager";
};

using NetworkFilterNames = ConstSingleton<NetworkFilterNameValues>;
Expand Down
8 changes: 7 additions & 1 deletion test/extensions/filters/network/reverse_tunnel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ envoy_extension_cc_test(
size = "large",
srcs = ["integration_test.cc"],
extension_names = [
"envoy.clusters.reverse_connection",
"envoy.filters.http.router",
"envoy.filters.network.reverse_tunnel",
"envoy.filters.network.reverse_tunnel_drain_aware_http_connection_manager",
"envoy.bootstrap.reverse_tunnel.upstream_socket_interface",
"envoy.bootstrap.reverse_tunnel.downstream_socket_interface",
"envoy.bootstrap.internal_listener",
"envoy.clusters.reverse_connection",
"envoy.resolvers.reverse_connection",
"envoy.filters.network.echo",
],
Expand All @@ -68,14 +70,18 @@ envoy_extension_cc_test(
"//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib",
"//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib",
"//source/extensions/clusters/reverse_connection:reverse_connection_lib",
"//source/extensions/filters/http/router:config",
"//source/extensions/filters/network/echo:config",
"//source/extensions/filters/network/reverse_tunnel:config",
"//source/extensions/filters/network/reverse_tunnel/drain_aware_hcm:drain_aware_config",
"//source/extensions/filters/network/set_filter_state:config",
"//source/extensions/transport_sockets/internal_upstream:config",
"//test/common/http/http2:http2_frame",
"//test/integration:integration_lib",
"//test/test_common:logging_lib",
"//test/test_common:utility_lib",
"@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto",
"@envoy_api//envoy/config/listener/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/transport_sockets/internal_upstream/v3:pkg_cc_proto",
Expand Down
Loading
Loading