Skip to content

Commit 3bae96b

Browse files
authored
reverse_tunnels: add validation in the network filter (#41271)
## Description This PR adds a `validation` to the Reverse Tunnel filter which could be used to do validations on the incoming Node ID and Cluster ID values in the reverse connection handshake. It's possible to use Filter State, SNI, Certificate SAN, etc. to do these validations by configuring the formatter. It's also possible to do validations on all or some of the inputs. --- **Commit Message:** reverse_tunnels: add validation in the network filter **Additional Description:** Adds `validation` to perform validations on the incoming Node ID and Cluster ID from reverse connection handshake. **Risk Level:** Low **Testing:** Added Unit + Integration Tests **Docs Changes:** N/A **Release Notes:** N/A --------- Signed-off-by: Rohit Agrawal <[email protected]>
1 parent 535da41 commit 3bae96b

File tree

9 files changed

+1206
-51
lines changed

9 files changed

+1206
-51
lines changed

api/envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.proto

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,98 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
1919
// Reverse Tunnel Network Filter :ref:`configuration overview <config_network_filters_reverse_tunnel>`.
2020
// [#extension: envoy.filters.network.reverse_tunnel]
2121

22+
// Validation configuration for reverse tunnel identifiers.
23+
// Validates the node ID and cluster ID extracted from reverse tunnel handshake headers
24+
// against expected values specified using format strings.
25+
message Validation {
26+
// Format string to extract the expected node identifier for validation.
27+
// The formatted value is compared against the ``x-envoy-reverse-tunnel-node-id`` header
28+
// from the incoming handshake request. If they do not match, the connection is rejected
29+
// with HTTP ``403 Forbidden``.
30+
//
31+
// Supports Envoy's :ref:`command operators <config_access_log_command_operators>`:
32+
//
33+
// * ``%DYNAMIC_METADATA(namespace:key)%``: Extract expected value from dynamic metadata.
34+
// * ``%FILTER_STATE(key)%``: Extract expected value from filter state.
35+
// * ``%DOWNSTREAM_REMOTE_ADDRESS%``: Use downstream connection IP address.
36+
// * Plain strings: Use a static expected value.
37+
//
38+
// If empty, node ID validation is skipped.
39+
//
40+
// Example using dynamic metadata allowlist:
41+
//
42+
// .. code-block:: yaml
43+
//
44+
// node_id_format: "%DYNAMIC_METADATA(envoy.reverse_tunnel.allowlist:expected_node_id)%"
45+
//
46+
string node_id_format = 1 [(validate.rules).string = {max_len: 1024}];
47+
48+
// Format string to extract the expected cluster identifier for validation.
49+
// The formatted value is compared against the ``x-envoy-reverse-tunnel-cluster-id`` header
50+
// from the incoming handshake request. If they do not match, the connection is rejected
51+
// with HTTP ``403 Forbidden``.
52+
//
53+
// Supports the same :ref:`command operators <config_access_log_command_operators>` as
54+
// ``node_id_format``.
55+
//
56+
// If empty, cluster ID validation is skipped.
57+
//
58+
// Example using filter state:
59+
//
60+
// .. code-block:: yaml
61+
//
62+
// cluster_id_format: "%FILTER_STATE(expected_cluster_id)%"
63+
//
64+
string cluster_id_format = 2 [(validate.rules).string = {max_len: 1024}];
65+
66+
// Whether to emit validation results as dynamic metadata.
67+
// When enabled, the filter emits metadata under the namespace specified by
68+
// ``dynamic_metadata_namespace`` containing:
69+
//
70+
// * ``node_id``: The actual node ID from the handshake request.
71+
// * ``cluster_id``: The actual cluster ID from the handshake request.
72+
// * ``validation_result``: Either ``allowed`` or ``denied``.
73+
//
74+
// This metadata can be used by subsequent filters or for access logging.
75+
// Defaults to ``false``.
76+
bool emit_dynamic_metadata = 3;
77+
78+
// Namespace for emitted dynamic metadata when ``emit_dynamic_metadata`` is ``true``.
79+
// If not specified, defaults to ``envoy.filters.network.reverse_tunnel``.
80+
string dynamic_metadata_namespace = 4 [(validate.rules).string = {max_len: 255}];
81+
}
82+
2283
// Configuration for the reverse tunnel network filter.
2384
// This filter handles reverse tunnel connection acceptance and rejection by processing
2485
// HTTP requests where required identification values are provided via HTTP headers.
86+
// [#next-free-field: 6]
2587
message ReverseTunnel {
2688
// Ping interval for health checks on established reverse tunnel connections.
27-
// If not specified, defaults to 2 seconds.
89+
// If not specified, defaults to ``2 seconds``.
2890
google.protobuf.Duration ping_interval = 1 [(validate.rules).duration = {
2991
lte {seconds: 300}
3092
gte {nanos: 1000000}
3193
}];
3294

3395
// Whether to automatically close connections after processing reverse tunnel requests.
34-
// When set to true, connections are closed after acceptance or rejection.
35-
// When set to false, connections remain open for potential reuse. Defaults to false.
96+
//
97+
// * When set to ``true``, connections are closed after acceptance or rejection.
98+
// * When set to ``false``, connections remain open for potential reuse.
99+
//
100+
// Defaults to ``false``.
36101
bool auto_close_connections = 2;
37102

38103
// HTTP path to match for reverse tunnel requests.
39-
// If not specified, defaults to "/reverse_connections/request".
104+
// If not specified, defaults to ``/reverse_connections/request``.
40105
string request_path = 3 [(validate.rules).string = {min_len: 1 max_len: 255 ignore_empty: true}];
41106

42107
// HTTP method to match for reverse tunnel requests.
43108
// If not specified (``METHOD_UNSPECIFIED``), this defaults to ``GET``.
44109
config.core.v3.RequestMethod request_method = 4 [(validate.rules).enum = {defined_only: true}];
110+
111+
// Optional validation configuration for node and cluster identifiers.
112+
// If specified, the filter validates the ``x-envoy-reverse-tunnel-node-id`` and
113+
// ``x-envoy-reverse-tunnel-cluster-id`` headers against expected values extracted
114+
// using format strings. Requests that fail validation are rejected with HTTP ``403 Forbidden``.
115+
Validation validation = 5;
45116
}

source/extensions/filters/network/reverse_tunnel/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ envoy_cc_library(
2727
hdrs = ["reverse_tunnel_filter.h"],
2828
deps = [
2929
"//envoy/buffer:buffer_interface",
30+
"//envoy/formatter:substitution_formatter_interface",
3031
"//envoy/http:codec_interface",
3132
"//envoy/network:connection_interface",
3233
"//envoy/network:filter_interface",
3334
"//envoy/ssl:connection_interface",
3435
"//envoy/thread_local:thread_local_interface",
3536
"//source/common/buffer:buffer_lib",
3637
"//source/common/common:logger_lib",
38+
"//source/common/formatter:substitution_format_string_lib",
39+
"//source/common/formatter:substitution_formatter_lib",
3740
"//source/common/http:codes_lib",
3841
"//source/common/http:header_map_lib",
3942
"//source/common/http:headers_lib",
@@ -51,7 +54,9 @@ envoy_cc_library(
5154
"//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_includes",
5255
"//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib",
5356
"//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:upstream_socket_manager_lib",
57+
"//source/server:generic_factory_context_lib",
5458
"//source/server:null_overload_manager_lib",
59+
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
5560
"@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto",
5661
],
5762
)

source/extensions/filters/network/reverse_tunnel/config.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ namespace Extensions {
77
namespace NetworkFilters {
88
namespace ReverseTunnel {
99

10-
Network::FilterFactoryCb ReverseTunnelFilterConfigFactory::createFilterFactoryFromProtoTyped(
10+
absl::StatusOr<Network::FilterFactoryCb>
11+
ReverseTunnelFilterConfigFactory::createFilterFactoryFromProtoTyped(
1112
const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config,
1213
Server::Configuration::FactoryContext& context) {
13-
auto config = std::make_shared<ReverseTunnelFilterConfig>(proto_config, context);
14+
auto config_or_error = ReverseTunnelFilterConfig::create(proto_config, context);
15+
if (!config_or_error.ok()) {
16+
return config_or_error.status();
17+
}
18+
auto config = config_or_error.value();
19+
1420
// Capture scope and overload manager pointers to avoid dangling references.
1521
Stats::Scope* scope = &context.scope();
1622
Server::OverloadManager* overload_manager = &context.serverFactoryContext().overloadManager();

source/extensions/filters/network/reverse_tunnel/config.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ namespace ReverseTunnel {
1515
* Config registration for the reverse tunnel network filter.
1616
*/
1717
class ReverseTunnelFilterConfigFactory
18-
: public Common::FactoryBase<
18+
: public Common::ExceptionFreeFactoryBase<
1919
envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel> {
2020
public:
2121
// Always mark the reverse tunnel filter as terminal filter.
2222
ReverseTunnelFilterConfigFactory()
23-
: FactoryBase(NetworkFilterNames::get().ReverseTunnel, true /* isTerminalFilter */) {}
23+
: ExceptionFreeFactoryBase(NetworkFilterNames::get().ReverseTunnel,
24+
true /* isTerminalFilter */) {}
2425

2526
private:
26-
Network::FilterFactoryCb createFilterFactoryFromProtoTyped(
27+
absl::StatusOr<Network::FilterFactoryCb> createFilterFactoryFromProtoTyped(
2728
const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config,
2829
Server::Configuration::FactoryContext& context) override;
2930
};

source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.cc

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
#include "source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h"
22

33
#include "envoy/buffer/buffer.h"
4+
#include "envoy/config/core/v3/substitution_format_string.pb.h"
45
#include "envoy/network/connection.h"
56
#include "envoy/server/overload/overload_manager.h"
67

78
#include "source/common/buffer/buffer_impl.h"
9+
#include "source/common/config/datasource.h"
10+
#include "source/common/formatter/substitution_format_string.h"
11+
#include "source/common/formatter/substitution_formatter.h"
812
#include "source/common/http/codes.h"
913
#include "source/common/http/header_map_impl.h"
1014
#include "source/common/http/headers.h"
@@ -15,6 +19,7 @@
1519
#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h"
1620
#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h"
1721
#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h"
22+
#include "source/server/generic_factory_context.h"
1823

1924
namespace Envoy {
2025
namespace Extensions {
@@ -29,9 +34,59 @@ ReverseTunnelFilter::ReverseTunnelStats::generateStats(const std::string& prefix
2934
}
3035

3136
// ReverseTunnelFilterConfig implementation.
37+
absl::StatusOr<std::shared_ptr<ReverseTunnelFilterConfig>> ReverseTunnelFilterConfig::create(
38+
const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config,
39+
Server::Configuration::FactoryContext& context) {
40+
41+
Formatter::FormatterConstSharedPtr node_id_formatter;
42+
Formatter::FormatterConstSharedPtr cluster_id_formatter;
43+
44+
// Create formatters for validation if configured.
45+
if (proto_config.has_validation()) {
46+
Server::GenericFactoryContextImpl generic_context(context.serverFactoryContext(),
47+
context.messageValidationVisitor());
48+
49+
const auto& validation = proto_config.validation();
50+
51+
// Create node_id formatter if configured.
52+
if (!validation.node_id_format().empty()) {
53+
envoy::config::core::v3::SubstitutionFormatString node_id_format_config;
54+
node_id_format_config.mutable_text_format_source()->set_inline_string(
55+
validation.node_id_format());
56+
57+
auto formatter_or_error = Formatter::SubstitutionFormatStringUtils::fromProtoConfig(
58+
node_id_format_config, generic_context);
59+
if (!formatter_or_error.ok()) {
60+
return absl::InvalidArgumentError(fmt::format("Failed to parse node_id_format: {}",
61+
formatter_or_error.status().message()));
62+
}
63+
node_id_formatter = std::move(formatter_or_error.value());
64+
}
65+
66+
// Create cluster_id formatter if configured.
67+
if (!validation.cluster_id_format().empty()) {
68+
envoy::config::core::v3::SubstitutionFormatString cluster_id_format_config;
69+
cluster_id_format_config.mutable_text_format_source()->set_inline_string(
70+
validation.cluster_id_format());
71+
72+
auto formatter_or_error = Formatter::SubstitutionFormatStringUtils::fromProtoConfig(
73+
cluster_id_format_config, generic_context);
74+
if (!formatter_or_error.ok()) {
75+
return absl::InvalidArgumentError(fmt::format("Failed to parse cluster_id_format: {}",
76+
formatter_or_error.status().message()));
77+
}
78+
cluster_id_formatter = std::move(formatter_or_error.value());
79+
}
80+
}
81+
82+
return std::shared_ptr<ReverseTunnelFilterConfig>(new ReverseTunnelFilterConfig(
83+
proto_config, std::move(node_id_formatter), std::move(cluster_id_formatter)));
84+
}
85+
3286
ReverseTunnelFilterConfig::ReverseTunnelFilterConfig(
3387
const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config,
34-
Server::Configuration::FactoryContext&)
88+
Formatter::FormatterConstSharedPtr node_id_formatter,
89+
Formatter::FormatterConstSharedPtr cluster_id_formatter)
3590
: ping_interval_(proto_config.has_ping_interval()
3691
? std::chrono::milliseconds(
3792
DurationUtil::durationToMilliseconds(proto_config.ping_interval()))
@@ -46,7 +101,77 @@ ReverseTunnelFilterConfig::ReverseTunnelFilterConfig(
46101
method = envoy::config::core::v3::GET;
47102
}
48103
return envoy::config::core::v3::RequestMethod_Name(method);
49-
}()) {}
104+
}()),
105+
node_id_formatter_(std::move(node_id_formatter)),
106+
cluster_id_formatter_(std::move(cluster_id_formatter)),
107+
emit_dynamic_metadata_(proto_config.has_validation() &&
108+
proto_config.validation().emit_dynamic_metadata()),
109+
dynamic_metadata_namespace_(
110+
proto_config.has_validation() &&
111+
!proto_config.validation().dynamic_metadata_namespace().empty()
112+
? proto_config.validation().dynamic_metadata_namespace()
113+
: "envoy.filters.network.reverse_tunnel") {}
114+
115+
bool ReverseTunnelFilterConfig::validateIdentifiers(
116+
absl::string_view node_id, absl::string_view cluster_id,
117+
const StreamInfo::StreamInfo& stream_info) const {
118+
119+
// If no validation configured, pass validation.
120+
if (!node_id_formatter_ && !cluster_id_formatter_) {
121+
return true;
122+
}
123+
124+
// Validate node_id if formatter is configured.
125+
if (node_id_formatter_) {
126+
const std::string expected_node_id = node_id_formatter_->formatWithContext({}, stream_info);
127+
if (!expected_node_id.empty() && expected_node_id != node_id) {
128+
ENVOY_LOG(debug, "reverse_tunnel: node_id validation failed. Expected: '{}', Actual: '{}'",
129+
expected_node_id, node_id);
130+
return false;
131+
}
132+
}
133+
134+
// Validate cluster_id if formatter is configured.
135+
if (cluster_id_formatter_) {
136+
const std::string expected_cluster_id =
137+
cluster_id_formatter_->formatWithContext({}, stream_info);
138+
if (!expected_cluster_id.empty() && expected_cluster_id != cluster_id) {
139+
ENVOY_LOG(debug, "reverse_tunnel: cluster_id validation failed. Expected: '{}', Actual: '{}'",
140+
expected_cluster_id, cluster_id);
141+
return false;
142+
}
143+
}
144+
145+
return true;
146+
}
147+
148+
void ReverseTunnelFilterConfig::emitValidationMetadata(absl::string_view node_id,
149+
absl::string_view cluster_id,
150+
bool validation_passed,
151+
StreamInfo::StreamInfo& stream_info) const {
152+
if (!emit_dynamic_metadata_) {
153+
return;
154+
}
155+
156+
Protobuf::Struct metadata;
157+
auto& fields = *metadata.mutable_fields();
158+
159+
// Emit actual identifiers.
160+
fields["node_id"].set_string_value(std::string(node_id));
161+
fields["cluster_id"].set_string_value(std::string(cluster_id));
162+
163+
// Emit validation result.
164+
fields["validation_result"].set_string_value(validation_passed ? "allowed" : "denied");
165+
166+
// Set dynamic metadata on the stream info.
167+
stream_info.setDynamicMetadata(dynamic_metadata_namespace_, metadata);
168+
169+
ENVOY_LOG(trace,
170+
"reverse_tunnel: emitted dynamic metadata to namespace '{}': node_id={}, "
171+
"cluster_id={}, validation_result={}",
172+
dynamic_metadata_namespace_, node_id, cluster_id,
173+
validation_passed ? "allowed" : "denied");
174+
}
50175

51176
// ReverseTunnelFilter implementation.
52177
ReverseTunnelFilter::ReverseTunnelFilter(ReverseTunnelFilterConfigSharedPtr config,
@@ -191,6 +316,25 @@ void ReverseTunnelFilter::RequestDecoderImpl::processIfComplete(bool end_stream)
191316
const absl::string_view cluster_id = cluster_vals[0]->value().getStringView();
192317
const absl::string_view tenant_id = tenant_vals[0]->value().getStringView();
193318

319+
// Validate node_id and cluster_id if validation is configured.
320+
auto& connection = parent_.read_callbacks_->connection();
321+
const bool validation_passed =
322+
parent_.config_->validateIdentifiers(node_id, cluster_id, connection.streamInfo());
323+
324+
// Emit validation metadata if configured.
325+
parent_.config_->emitValidationMetadata(node_id, cluster_id, validation_passed,
326+
connection.streamInfo());
327+
328+
if (!validation_passed) {
329+
parent_.stats_.validation_failed_.inc();
330+
ENVOY_CONN_LOG(debug, "reverse_tunnel: validation failed for node '{}', cluster '{}'",
331+
parent_.read_callbacks_->connection(), node_id, cluster_id);
332+
sendLocalReply(Http::Code::Forbidden, "Validation failed", nullptr, absl::nullopt,
333+
"reverse_tunnel_validation_failed");
334+
parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite);
335+
return;
336+
}
337+
194338
// Respond with 200 OK.
195339
auto resp_headers = Http::ResponseHeaderMapImpl::create();
196340
resp_headers->setStatus(200);

0 commit comments

Comments
 (0)