Skip to content
Merged
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
21 changes: 16 additions & 5 deletions api/envoy/config/core/v3/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,26 @@ message UpstreamHttpProtocolOptions {
"envoy.api.v2.core.UpstreamHttpProtocolOptions";

// Set transport socket `SNI <https://en.wikipedia.org/wiki/Server_Name_Indication>`_ for new
// upstream connections based on the downstream HTTP host/authority header, as seen by the
// :ref:`router filter <config_http_filters_router>`.
// upstream connections based on the downstream HTTP host/authority header or any other arbitrary
// header when :ref:`override_auto_sni_header <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.override_auto_sni_header>`
// is set, as seen by the :ref:`router filter <config_http_filters_router>`.
bool auto_sni = 1;

// Automatic validate upstream presented certificate for new upstream connections based on the
// downstream HTTP host/authority header, as seen by the
// :ref:`router filter <config_http_filters_router>`.
// This field is intended to set with `auto_sni` field.
// downstream HTTP host/authority header or any other arbitrary header when :ref:`override_auto_sni_header <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.override_auto_sni_header>`
// is set, as seen by the :ref:`router filter <config_http_filters_router>`.
// This field is intended to be set with `auto_sni` field.
bool auto_san_validation = 2;

// An optional alternative to the host/authority header to be used for setting the SNI value.
// It should be a valid downstream HTTP header, as seen by the
// :ref:`router filter <config_http_filters_router>`.
// If unset, host/authority header will be used for populating the SNI. If the specified header
// is not found or the value is empty, host/authority header will be used instead.
// This field is intended to be set with `auto_sni` and/or `auto_san_validation` fields.
// If none of these fields are set then setting this would be a no-op.
string override_auto_sni_header = 3
[(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}];
}

// Configures the alternate protocols cache which tracks alternate protocols that can be used to
Expand Down
6 changes: 4 additions & 2 deletions docs/root/faq/configuration/sni.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ How do I configure SNI for clusters?
====================================

For clusters, a fixed SNI can be set in :ref:`UpstreamTlsContext <envoy_v3_api_field_extensions.transport_sockets.tls.v3.UpstreamTlsContext.sni>`.
To derive SNI from HTTP ``host`` or ``:authority`` header, turn on
To derive SNI from a downstream HTTP header like, ``host`` or ``:authority``, turn on
:ref:`auto_sni <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.auto_sni>` to override the fixed SNI in
`UpstreamTlsContext`. If upstream will present certificates with the hostname in SAN, turn on
`UpstreamTlsContext`. A custom header other than the ``host`` or ``:authority`` can also be supplied using the optional
:ref:`override_auto_sni_header <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.override_auto_sni_header>` field.
If upstream will present certificates with the hostname in SAN, turn on
:ref:`auto_san_validation <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.auto_san_validation>` too.
It still needs a trust CA in validation context in ``UpstreamTlsContext`` for trust anchor.
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ New Features
* overload: add a new overload action that resets streams using a lot of memory. To enable the tracking of allocated bytes in buffers that a stream is using we need to configure the minimum threshold for tracking via:ref:`buffer_factory_config <envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`. We have an overload action ``Envoy::Server::OverloadActionNameValues::ResetStreams`` that takes advantage of the tracking to reset the most expensive stream first.
* rbac: added :ref:`destination_port_range <envoy_v3_api_field_config.rbac.v3.Permission.destination_port_range>` for matching range of destination ports.
* route config: added :ref:`dynamic_metadata <envoy_v3_api_field_config.route.v3.RouteMatch.dynamic_metadata>` for routing based on dynamic metadata.
* router: added an optional :ref:`override_auto_sni_header <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.override_auto_sni_header>` to support setting SNI value from an arbitrary header other than host/authority.
* sxg_filter: added filter to transform response to SXG package to :ref:`contrib images <install_contrib>`. This can be enabled by setting :ref:`SXG <envoy_v3_api_msg_extensions.filters.http.sxg.v3alpha.SXG>` configuration.
* thrift_proxy: added support for :ref:`mirroring requests <envoy_v3_api_field_extensions.filters.network.thrift_proxy.v3.RouteAction.request_mirror_policies>`.

Expand Down
31 changes: 26 additions & 5 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -502,20 +502,41 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
// Fetch a connection pool for the upstream cluster.
const auto& upstream_http_protocol_options = cluster_->upstreamHttpProtocolOptions();

if (upstream_http_protocol_options.has_value()) {
const auto parsed_authority = Http::Utility::parseAuthority(headers.getHostValue());
if (!parsed_authority.is_ip_address_ && upstream_http_protocol_options.value().auto_sni()) {
if (upstream_http_protocol_options.has_value() &&
(upstream_http_protocol_options.value().auto_sni() ||
upstream_http_protocol_options.value().auto_san_validation())) {
// Default the header to Host/Authority header.
absl::string_view header_value = headers.getHostValue();

// Check whether `override_auto_sni_header` is specified.
const auto override_auto_sni_header =
upstream_http_protocol_options.value().override_auto_sni_header();
if (!override_auto_sni_header.empty()) {
// Use the header value from `override_auto_sni_header` to set the SNI value.
const auto overridden_header_value = Http::HeaderUtility::getAllOfHeaderAsString(
headers, Http::LowerCaseString(override_auto_sni_header));
if (overridden_header_value.result().has_value() &&
!overridden_header_value.result().value().empty()) {
header_value = overridden_header_value.result().value();
}
}
const auto parsed_authority = Http::Utility::parseAuthority(header_value);
bool should_set_sni = !parsed_authority.is_ip_address_;
// `host_` returns a string_view so doing this should be safe.
absl::string_view sni_value = parsed_authority.host_;

if (should_set_sni && upstream_http_protocol_options.value().auto_sni()) {
callbacks_->streamInfo().filterState()->setData(
Network::UpstreamServerName::key(),
std::make_unique<Network::UpstreamServerName>(parsed_authority.host_),
std::make_unique<Network::UpstreamServerName>(sni_value),
StreamInfo::FilterState::StateType::Mutable);
}

if (upstream_http_protocol_options.value().auto_san_validation()) {
callbacks_->streamInfo().filterState()->setData(
Network::UpstreamSubjectAltNames::key(),
std::make_unique<Network::UpstreamSubjectAltNames>(
std::vector<std::string>{std::string(parsed_authority.host_)}),
std::vector<std::string>{std::string(sni_value)}),
StreamInfo::FilterState::StateType::Mutable);
}
}
Expand Down
159 changes: 112 additions & 47 deletions test/common/router/router_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,65 +141,130 @@ class RouterTest : public RouterTestBase {

router_.onDestroy();
}

void testAutoSniOptions(
absl::optional<envoy::config::core::v3::UpstreamHttpProtocolOptions> dummy_option,
Envoy::Http::TestRequestHeaderMapImpl headers, std::string server_name = "host",
bool should_validate_san = false, std::string alt_server_name = "host") {
NiceMock<StreamInfo::MockStreamInfo> stream_info;
ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, upstreamHttpProtocolOptions())
.WillByDefault(ReturnRef(dummy_option));
ON_CALL(callbacks_.stream_info_, filterState())
.WillByDefault(ReturnRef(stream_info.filterState()));
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _))
.WillOnce(Return(&cancellable_));
stream_info.filterState()->setData(Network::UpstreamServerName::key(),
std::make_unique<Network::UpstreamServerName>("dummy"),
StreamInfo::FilterState::StateType::Mutable);
expectResponseTimerCreate();

HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, true);
EXPECT_EQ(server_name,
stream_info.filterState()
->getDataReadOnly<Network::UpstreamServerName>(Network::UpstreamServerName::key())
.value());
if (should_validate_san) {
EXPECT_EQ(alt_server_name, stream_info.filterState()
->getDataReadOnly<Network::UpstreamSubjectAltNames>(
Network::UpstreamSubjectAltNames::key())
.value()[0]);
}
EXPECT_CALL(cancellable_, cancel(_));
router_.onDestroy();
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
EXPECT_EQ(0U,
callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value());
EXPECT_EQ(0U,
callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value());
}
};

TEST_F(RouterTest, UpdateServerNameFilterState) {
NiceMock<StreamInfo::MockStreamInfo> stream_info;
TEST_F(RouterTest, UpdateServerNameFilterStateWithoutHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, upstreamHttpProtocolOptions())
.WillByDefault(ReturnRef(dummy_option));
ON_CALL(callbacks_.stream_info_, filterState())
.WillByDefault(ReturnRef(stream_info.filterState()));
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _))
.WillOnce(Return(&cancellable_));
stream_info.filterState()->setData(Network::UpstreamServerName::key(),
std::make_unique<Network::UpstreamServerName>("dummy"),
StreamInfo::FilterState::StateType::Mutable);
expectResponseTimerCreate();

Http::TestRequestHeaderMapImpl headers;
Http::TestRequestHeaderMapImpl headers{};
testAutoSniOptions(dummy_option, headers);
}

HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, true);
EXPECT_EQ("host",
stream_info.filterState()
->getDataReadOnly<Network::UpstreamServerName>(Network::UpstreamServerName::key())
.value());
EXPECT_CALL(cancellable_, cancel(_));
router_.onDestroy();
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
EXPECT_EQ(0U,
callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value());
EXPECT_EQ(0U,
callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value());
TEST_F(RouterTest, UpdateServerNameFilterStateWithHostHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_override_auto_sni_header(":authority");

Http::TestRequestHeaderMapImpl headers{};
testAutoSniOptions(dummy_option, headers);
}

TEST_F(RouterTest, UpdateServerNameFilterStateWithHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_override_auto_sni_header("x-host");

const auto server_name = "foo.bar";
Http::TestRequestHeaderMapImpl headers{{"x-host", server_name}};
testAutoSniOptions(dummy_option, headers, server_name);
}

TEST_F(RouterTest, UpdateServerNameFilterStateWithEmptyValueHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_override_auto_sni_header("x-host");

Http::TestRequestHeaderMapImpl headers{{"x-host", ""}};
testAutoSniOptions(dummy_option, headers);
}

TEST_F(RouterTest, UpdateSubjectAltNamesFilterState) {
NiceMock<StreamInfo::MockStreamInfo> stream_info;
TEST_F(RouterTest, UpdateSubjectAltNamesFilterStateWithoutHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_auto_san_validation(true);
ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, upstreamHttpProtocolOptions())
.WillByDefault(ReturnRef(dummy_option));
ON_CALL(callbacks_.stream_info_, filterState())
.WillByDefault(ReturnRef(stream_info.filterState()));
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _))
.WillOnce(Return(&cancellable_));
expectResponseTimerCreate();

Http::TestRequestHeaderMapImpl headers;
Http::TestRequestHeaderMapImpl headers{};
testAutoSniOptions(dummy_option, headers, "host", true);
}

HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, true);
EXPECT_EQ("host", stream_info.filterState()
->getDataReadOnly<Network::UpstreamSubjectAltNames>(
Network::UpstreamSubjectAltNames::key())
.value()[0]);
EXPECT_CALL(cancellable_, cancel(_));
router_.onDestroy();
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
EXPECT_EQ(0U,
callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value());
TEST_F(RouterTest, UpdateSubjectAltNamesFilterStateWithHostHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_auto_san_validation(true);
dummy_option.value().set_override_auto_sni_header(":authority");

Http::TestRequestHeaderMapImpl headers{};
testAutoSniOptions(dummy_option, headers, "host", true);
}

TEST_F(RouterTest, UpdateSubjectAltNamesFilterStateWithHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_auto_san_validation(true);
dummy_option.value().set_override_auto_sni_header("x-host");

const auto server_name = "foo.bar";
Http::TestRequestHeaderMapImpl headers{{"x-host", server_name}};
testAutoSniOptions(dummy_option, headers, server_name, true, server_name);
}

TEST_F(RouterTest, UpdateSubjectAltNamesFilterStateWithEmptyValueHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_auto_san_validation(true);
dummy_option.value().set_override_auto_sni_header("x-host");

Http::TestRequestHeaderMapImpl headers{{"x-host", ""}};
testAutoSniOptions(dummy_option, headers, "host", true);
}

TEST_F(RouterTest, UpdateSubjectAltNamesFilterStateWithIpHeaderOverride) {
auto dummy_option = absl::make_optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>();
dummy_option.value().set_auto_sni(true);
dummy_option.value().set_auto_san_validation(true);
dummy_option.value().set_override_auto_sni_header("x-host");

const auto server_name = "127.0.0.1";
Http::TestRequestHeaderMapImpl headers{{"x-host", server_name}};
testAutoSniOptions(dummy_option, headers, "dummy", true, server_name);
}

TEST_F(RouterTest, RouteNotFound) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class ProxyFilterIntegrationTest : public testing::TestWithParam<Network::Addres
public:
ProxyFilterIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {}

void initializeWithArgs(uint64_t max_hosts = 1024, uint32_t max_pending_requests = 1024) {
void initializeWithArgs(uint64_t max_hosts = 1024, uint32_t max_pending_requests = 1024,
const std::string& override_auto_sni_header = "") {
setUpstreamProtocol(Http::CodecType::HTTP1);
const std::string filename = TestEnvironment::temporaryPath("dns_cache.txt");

Expand Down Expand Up @@ -53,8 +54,11 @@ name: dynamic_forward_proxy

// Set validate_clusters to false to allow us to reference a CDS cluster.
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) { hcm.mutable_route_config()->mutable_validate_clusters()->set_value(false); });
[override_auto_sni_header](
envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) {
hcm.mutable_route_config()->mutable_validate_clusters()->set_value(false);
});

// Setup the initial CDS cluster.
cluster_.mutable_connect_timeout()->CopyFrom(
Expand All @@ -64,6 +68,10 @@ name: dynamic_forward_proxy

ConfigHelper::HttpProtocolOptions protocol_options;
protocol_options.mutable_upstream_http_protocol_options()->set_auto_sni(true);
if (!override_auto_sni_header.empty()) {
protocol_options.mutable_upstream_http_protocol_options()->set_override_auto_sni_header(
override_auto_sni_header);
}
protocol_options.mutable_upstream_http_protocol_options()->set_auto_san_validation(true);
protocol_options.mutable_explicit_http_config()->mutable_http_protocol_options();
ConfigHelper::setProtocolOptions(cluster_, protocol_options);
Expand Down Expand Up @@ -279,6 +287,34 @@ TEST_P(ProxyFilterIntegrationTest, UpstreamTls) {
checkSimpleRequestSuccess(0, 0, response.get());
}

// Verify that `override_auto_sni_header` can be used along with auto_sni to set
// SNI from an arbitrary header.
TEST_P(ProxyFilterIntegrationTest, UpstreamTlsWithAltHeaderSni) {
upstream_tls_ = true;
initializeWithArgs(1024, 1024, "x-host");
codec_client_ = makeHttpConnection(lookupPort("http"));
const Http::TestRequestHeaderMapImpl request_headers{
{":method", "POST"},
{":path", "/test/long/url"},
{":scheme", "http"},
{":authority",
fmt::format("{}:{}", fake_upstreams_[0]->localAddress()->ip()->addressAsString().c_str(),
fake_upstreams_[0]->localAddress()->ip()->port())},
{"x-host", "localhost"}};

auto response = codec_client_->makeHeaderOnlyRequest(request_headers);
waitForNextUpstreamRequest();

const Extensions::TransportSockets::Tls::SslHandshakerImpl* ssl_socket =
dynamic_cast<const Extensions::TransportSockets::Tls::SslHandshakerImpl*>(
fake_upstream_connection_->connection().ssl().get());
EXPECT_STREQ("localhost", SSL_get_servername(ssl_socket->ssl(), TLSEXT_NAMETYPE_host_name));

upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
checkSimpleRequestSuccess(0, 0, response.get());
}

TEST_P(ProxyFilterIntegrationTest, UpstreamTlsWithIpHost) {
upstream_tls_ = true;
initializeWithArgs();
Expand Down
Loading