Skip to content
Open
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
19 changes: 18 additions & 1 deletion api/envoy/config/route/v3/route_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ message RouteAction {
// .. note::
//
// Shadowing doesn't support HTTP CONNECT and upgrades.
// [#next-free-field: 9]
// [#next-free-field: 10]
message RequestMirrorPolicy {
option (udpa.annotations.versioning).previous_message_type =
"envoy.api.v2.route.RouteAction.RequestMirrorPolicy";
Expand Down Expand Up @@ -920,6 +920,23 @@ message RouteAction {
// is implicitly enabled if this field is set.
string host_rewrite_literal = 8
[(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}];

// Indicates that the host header of the mirrored request should be rewritten to the hostname
// of the upstream host chosen by the shadow cluster's load balancer. This has the same
// semantics as :ref:`auto_host_rewrite
// <envoy_v3_api_field_config.route.v3.RouteAction.auto_host_rewrite>` on the main route.
// If the selected upstream host has no hostname (e.g. a STATIC cluster with only IP endpoints),
// the host header is left unchanged.
//
// This field is mutually exclusive with
// :ref:`host_rewrite_literal
// <envoy_v3_api_field_config.route.v3.RouteAction.RequestMirrorPolicy.host_rewrite_literal>`.
// If both are set, ``host_rewrite_literal`` takes precedence.
//
// :ref:`disable_shadow_host_suffix_append
// <envoy_v3_api_field_config.route.v3.RouteAction.RequestMirrorPolicy.disable_shadow_host_suffix_append>`
// is implicitly enabled if this field is set.
google.protobuf.BoolValue auto_host_rewrite = 9;
}

// Specifies the route's hashing policy if the upstream cluster uses a hashing :ref:`load balancer
Expand Down
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@ removed_config_or_runtime:
and legacy code path.

new_features:
- area: router
change: |
Added :ref:`auto_host_rewrite
<envoy_v3_api_field_config.route.v3.RouteAction.RequestMirrorPolicy.auto_host_rewrite>`
support to :ref:`RequestMirrorPolicy
<envoy_v3_api_msg_config.route.v3.RouteAction.RequestMirrorPolicy>`, allowing mirrored
requests to rewrite the host header to the upstream host.
- area: lua
change: |
Added stats API support to the :ref:`Lua filter <config_http_filters_lua>`, allowing Lua
Expand Down
8 changes: 8 additions & 0 deletions envoy/http/async_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ class AsyncClient {
return *this;
}

StreamOptions& setAutoHostRewrite(bool r) {
auto_host_rewrite = r;
return *this;
}

StreamOptions& setParentSpan(Tracing::Span& parent_span) {
parent_span_ = &parent_span;
return *this;
Expand Down Expand Up @@ -446,6 +451,9 @@ class AsyncClient {
bool is_shadow{false};

bool is_shadow_suffixed_disabled{false};

bool auto_host_rewrite{false};

bool discard_response_body{false};

// The parent span that child spans are created under to trace egress requests/responses.
Expand Down
6 changes: 6 additions & 0 deletions envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,12 @@ class ShadowPolicy {
* @return the literal value to rewrite the host header with, or empty if no rewrite.
*/
virtual absl::string_view hostRewriteLiteral() const PURE;

/**
* @return true if the host header should be rewritten to the hostname of the upstream host
* selected by the shadow cluster's load balancer.
*/
virtual bool autoHostRewrite() const PURE;
};

using ShadowPolicyPtr = std::shared_ptr<ShadowPolicy>;
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/async_client_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal

auto route_or_error = NullRouteImpl::create(
parent_.cluster_->name(), std::move(retry_policy), parent_.factory_context_.regexEngine(),
options.timeout, options.hash_policy, metadata_matching_criteria);
options.timeout, options.hash_policy, metadata_matching_criteria, options.auto_host_rewrite);
SET_AND_RETURN_IF_NOT_OK(route_or_error.status(), creation_status);
route_ = std::move(*route_or_error);
stream_info_.dynamicMetadata().MergeFrom(options.metadata);
Expand Down
26 changes: 15 additions & 11 deletions source/common/http/null_route_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ struct RouteEntryImpl : public Router::RouteEntry {
const Protobuf::RepeatedPtrField<envoy::config::route::v3::RouteAction::HashPolicy>&
hash_policy,
Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine,
const Router::MetadataMatchCriteria* metadata_match) {
const Router::MetadataMatchCriteria* metadata_match, bool auto_host_rewrite = false) {
absl::Status creation_status = absl::OkStatus();
auto ret = std::unique_ptr<RouteEntryImpl>(
new RouteEntryImpl(cluster_name, timeout, hash_policy, std::move(retry_policy),
regex_engine, creation_status, metadata_match));
regex_engine, creation_status, metadata_match, auto_host_rewrite));
RETURN_IF_NOT_OK(creation_status);
return ret;
}
Expand All @@ -110,9 +110,10 @@ struct RouteEntryImpl : public Router::RouteEntry {
const Protobuf::RepeatedPtrField<envoy::config::route::v3::RouteAction::HashPolicy>&
hash_policy,
Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine,
absl::Status& creation_status, const Router::MetadataMatchCriteria* metadata_match)
absl::Status& creation_status, const Router::MetadataMatchCriteria* metadata_match,
bool auto_host_rewrite = false)
: metadata_match_(metadata_match), retry_policy_(std::move(retry_policy)),
cluster_name_(cluster_name), timeout_(timeout) {
cluster_name_(cluster_name), timeout_(timeout), auto_host_rewrite_(auto_host_rewrite) {
if (!hash_policy.empty()) {
auto policy_or_error = HashPolicyImpl::create(hash_policy, regex_engine);
SET_AND_RETURN_IF_NOT_OK(policy_or_error.status(), creation_status);
Expand Down Expand Up @@ -195,7 +196,7 @@ struct RouteEntryImpl : public Router::RouteEntry {
const std::multimap<std::string, std::string>& opaqueConfig() const override {
return opaque_config_;
}
bool autoHostRewrite() const override { return false; }
bool autoHostRewrite() const override { return auto_host_rewrite_; }
bool appendXfh() const override { return false; }
bool includeVirtualHostRateLimits() const override { return true; }
const Router::PathMatchCriterion& pathMatchCriterion() const override {
Expand Down Expand Up @@ -227,6 +228,7 @@ struct RouteEntryImpl : public Router::RouteEntry {
Router::RouteEntry::UpgradeMap upgrade_map_;
const std::string cluster_name_;
absl::optional<std::chrono::milliseconds> timeout_;
const bool auto_host_rewrite_;
static const ConnectConfigOptRef connect_config_nullopt_;
// Pass early data option config through StreamOptions.
std::unique_ptr<Router::EarlyDataPolicy> early_data_policy_{
Expand All @@ -239,11 +241,12 @@ struct NullRouteImpl : public Router::Route {
Regex::Engine& regex_engine, const absl::optional<std::chrono::milliseconds>& timeout = {},
const Protobuf::RepeatedPtrField<envoy::config::route::v3::RouteAction::HashPolicy>&
hash_policy = {},
const Router::MetadataMatchCriteria* metadata_match = nullptr) {
const Router::MetadataMatchCriteria* metadata_match = nullptr,
bool auto_host_rewrite = false) {
absl::Status creation_status;
auto ret = std::unique_ptr<NullRouteImpl>(
new NullRouteImpl(cluster_name, std::move(retry_policy), regex_engine, timeout, hash_policy,
creation_status, metadata_match));
creation_status, metadata_match, auto_host_rewrite));
RETURN_IF_NOT_OK(creation_status);
return ret;
}
Expand Down Expand Up @@ -280,10 +283,11 @@ struct NullRouteImpl : public Router::Route {
const absl::optional<std::chrono::milliseconds>& timeout,
const Protobuf::RepeatedPtrField<envoy::config::route::v3::RouteAction::HashPolicy>&
hash_policy,
absl::Status& creation_status,
const Router::MetadataMatchCriteria* metadata_match) {
auto entry_or_error = RouteEntryImpl::create(
cluster_name, timeout, hash_policy, std::move(retry_policy), regex_engine, metadata_match);
absl::Status& creation_status, const Router::MetadataMatchCriteria* metadata_match,
bool auto_host_rewrite = false) {
auto entry_or_error =
RouteEntryImpl::create(cluster_name, timeout, hash_policy, std::move(retry_policy),
regex_engine, metadata_match, auto_host_rewrite);
SET_AND_RETURN_IF_NOT_OK(entry_or_error.status(), creation_status);
route_entry_ = std::move(*entry_or_error);
}
Expand Down
3 changes: 2 additions & 1 deletion source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ ShadowPolicyImpl::ShadowPolicyImpl(const RequestMirrorPolicy& config,
absl::Status& creation_status)
: cluster_(config.cluster()), cluster_header_(config.cluster_header()),
disable_shadow_host_suffix_append_(config.disable_shadow_host_suffix_append()),
host_rewrite_literal_(config.host_rewrite_literal()) {
host_rewrite_literal_(config.host_rewrite_literal()),
auto_host_rewrite_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, auto_host_rewrite, false)) {
SET_AND_RETURN_IF_NOT_OK(validateMirrorClusterSpecifier(config), creation_status);

if (config.has_runtime_fraction()) {
Expand Down
5 changes: 4 additions & 1 deletion source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,12 @@ class ShadowPolicyImpl : public ShadowPolicy {
const envoy::type::v3::FractionalPercent& defaultValue() const override { return default_value_; }
absl::optional<bool> traceSampled() const override { return trace_sampled_; }
bool disableShadowHostSuffixAppend() const override {
return disable_shadow_host_suffix_append_ || !host_rewrite_literal_.empty();
return disable_shadow_host_suffix_append_ || !host_rewrite_literal_.empty() ||
auto_host_rewrite_;
}
const Http::HeaderEvaluator& headerEvaluator() const override;
absl::string_view hostRewriteLiteral() const override { return host_rewrite_literal_; }
bool autoHostRewrite() const override { return auto_host_rewrite_; }

private:
explicit ShadowPolicyImpl(const RequestMirrorPolicy& config,
Expand All @@ -435,6 +437,7 @@ class ShadowPolicyImpl : public ShadowPolicy {
absl::optional<bool> trace_sampled_;
const bool disable_shadow_host_suffix_append_;
const std::string host_rewrite_literal_;
const bool auto_host_rewrite_;
HeaderMutationsPtr request_headers_mutations_;
};

Expand Down
1 change: 1 addition & 0 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster,
.setSampled(shadow_policy.traceSampled())
.setIsShadow(true)
.setIsShadowSuffixDisabled(shadow_policy.disableShadowHostSuffixAppend())
.setAutoHostRewrite(shadow_policy.autoHostRewrite())
.setBufferAccount(callbacks_->account())
// Calculate effective buffer limit for shadow streams using the same logic as main
// request. A buffer limit of 1 is set in the case that the effective limit == 0,
Expand Down
46 changes: 46 additions & 0 deletions test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4802,6 +4802,52 @@ TEST_F(RouteMatcherTest, RequestMirrorPoliciesWithHeaders) {
EXPECT_EQ("existing-value", headers.get_("x-existing-header"));
}

// Test that auto_host_rewrite is correctly parsed and reflected in the shadow policy, and that it
// implicitly disables the "-shadow" suffix append (similar to host_rewrite_literal).
TEST_F(RouteMatcherTest, RequestMirrorPoliciesAutoHostRewrite) {
const std::string yaml = R"EOF(
virtual_hosts:
- name: www2
domains:
- www.lyft.com
routes:
- match:
prefix: "/auto-rewrite"
route:
request_mirror_policies:
- cluster: some_cluster
auto_host_rewrite: true
cluster: www2
- match:
prefix: "/no-rewrite"
route:
request_mirror_policies:
- cluster: some_cluster
cluster: www2
)EOF";

factory_context_.cluster_manager_.initializeClusters({"www2", "some_cluster"}, {});
TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true,
creation_status_);

const auto& auto_rewrite_policies =
config.route(genHeaders("www.lyft.com", "/auto-rewrite", "GET"), 0)
->routeEntry()
->shadowPolicies();
ASSERT_EQ(1, auto_rewrite_policies.size());
EXPECT_TRUE(auto_rewrite_policies[0]->autoHostRewrite());
// auto_host_rewrite implicitly disables the "-shadow" suffix.
EXPECT_TRUE(auto_rewrite_policies[0]->disableShadowHostSuffixAppend());

const auto& no_rewrite_policies =
config.route(genHeaders("www.lyft.com", "/no-rewrite", "GET"), 0)
->routeEntry()
->shadowPolicies();
ASSERT_EQ(1, no_rewrite_policies.size());
EXPECT_FALSE(no_rewrite_policies[0]->autoHostRewrite());
EXPECT_FALSE(no_rewrite_policies[0]->disableShadowHostSuffixAppend());
}

// Test if the higher level mirror policies are properly applied when routes
// don't have one and not applied when they do.
// In this test case, request_mirror_policies is set in route config level.
Expand Down
3 changes: 3 additions & 0 deletions test/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,15 @@ envoy_cc_test(
":http_integration_lib",
":integration_lib",
":socket_interface_swap_lib",
"//source/extensions/clusters/dns:dns_cluster_lib",
"//source/extensions/load_balancing_policies/subset:config",
"//source/extensions/network/dns_resolver/getaddrinfo:config",
"//test/integration/filters:add_header_filter_config_lib",
"//test/integration/filters:encoder_decoder_buffer_filter_lib",
"//test/integration/filters:on_local_reply_filter_config_lib",
"//test/integration/filters:repick_cluster_filter_lib",
"//test/test_common:test_runtime_lib",
"//test/test_common:threadsafe_singleton_injector_lib",
"@envoy_api//envoy/extensions/access_loggers/file/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/filters/http/router/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/filters/http/upstream_codec/v3:pkg_cc_proto",
Expand Down
84 changes: 84 additions & 0 deletions test/integration/shadow_policy_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
#include "envoy/extensions/filters/http/upstream_codec/v3/upstream_codec.pb.h"
#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"

#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h"

#include "test/integration/filters/repick_cluster_filter.h"
#include "test/integration/http_integration.h"
#include "test/integration/socket_interface_swap.h"
#include "test/integration/utility.h"
#include "test/test_common/test_runtime.h"
#include "test/test_common/threadsafe_singleton_injector.h"

namespace Envoy {
namespace {
Expand Down Expand Up @@ -1120,5 +1124,85 @@ TEST_P(ShadowPolicyIntegrationTest, ShadowWithHeaderManipulation) {
cleanupUpstreamAndDownstream();
}

// When auto_host_rewrite is set on a mirror policy with a LOGICAL_DNS cluster, the Host header of
// the mirrored request should be rewritten to the cluster's upstream hostname.
TEST_P(ShadowPolicyIntegrationTest, ShadowedClusterAutoHostRewriteLogicalDns) {
// TODO(#27132): auto_host_rewrite is broken for IPv6 addresses (UHV validation failure).
if (version_ == Network::Address::IpVersion::v6) {
return;
}

OsSysCallsWithMockedDns mock_os_sys_calls;
mock_os_sys_calls.setIpVersion(version_);
TestThreadsafeSingletonInjector<Api::OsSysCallsImpl> os_calls{&mock_os_sys_calls};

initialConfigSetup("cluster_1", "");
config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
// Convert cluster_1 (index 1, added by initialConfigSetup) to LOGICAL_DNS and set an explicit
// endpoint hostname. ConfigHelper will still rewrite the socket address to the loopback IP,
// so DNS resolution succeeds, but the logical host's hostname comes from the endpoint field.
auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(1);
cluster->set_type(envoy::config::cluster::v3::Cluster::LOGICAL_DNS);
cluster->set_dns_lookup_family(envoy::config::cluster::v3::Cluster::V4_ONLY);
auto* typed_dns_resolver_config = cluster->mutable_typed_dns_resolver_config();
typed_dns_resolver_config->set_name("envoy.network.dns_resolver.getaddrinfo");
envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig
getaddrinfo_config;
typed_dns_resolver_config->mutable_typed_config()->PackFrom(getaddrinfo_config);
// Set endpoint hostname so LogicalDnsCluster::hostname_ is "shadow.example.com" rather than
// the raw DNS address.
cluster->mutable_load_assignment()
->mutable_endpoints(0)
->mutable_lb_endpoints(0)
->mutable_endpoint()
->set_hostname("shadow.example.com");
});
config_helper_.addConfigModifier(
[=](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void {
auto* mirror_policy = hcm.mutable_route_config()
->mutable_virtual_hosts(0)
->mutable_routes(0)
->mutable_route()
->add_request_mirror_policies();
mirror_policy->set_cluster("cluster_1");
mirror_policy->mutable_auto_host_rewrite()->set_value(true);
});

initialize();
sendRequestAndValidateResponse();

EXPECT_EQ(upstream_headers_->Host()->value().getStringView(), "sni.lyft.com");
// The mirrored request's Host header should be rewritten to the shadow cluster's upstream
// hostname, not the downstream request's host and not the "-shadow"-suffixed version.
EXPECT_EQ(mirror_headers_->Host()->value().getStringView(), "shadow.example.com");
}

// When auto_host_rewrite is set on a mirror policy with a STATIC cluster (which has no hostname),
// the "-shadow" suffix should be suppressed (same as disable_shadow_host_suffix_append) and the
// Host header should remain unchanged since there is no upstream hostname to rewrite to.
TEST_P(ShadowPolicyIntegrationTest, ShadowedClusterAutoHostRewriteStaticCluster) {
initialConfigSetup("cluster_1", "");
config_helper_.addConfigModifier(
[=](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void {
auto* mirror_policy = hcm.mutable_route_config()
->mutable_virtual_hosts(0)
->mutable_routes(0)
->mutable_route()
->add_request_mirror_policies();
mirror_policy->set_cluster("cluster_1");
mirror_policy->mutable_auto_host_rewrite()->set_value(true);
});

initialize();
sendRequestAndValidateResponse();

// auto_host_rewrite on a STATIC cluster suppresses the "-shadow" suffix (no hostname to rewrite
// to, so the host header is preserved as-is from the downstream request).
EXPECT_EQ(upstream_headers_->Host()->value().getStringView(), "sni.lyft.com");
EXPECT_EQ(mirror_headers_->Host()->value().getStringView(), "sni.lyft.com");
}

} // namespace
} // namespace Envoy
Loading