Skip to content

Commit 877b982

Browse files
agrawrohyanavlasov
andauthored
dfp: add support for DFP honoring dynamic_host filter state when configured (#39605)
Signed-off-by: Rohit Agrawal <[email protected]> Co-authored-by: yanavlasov <[email protected]>
1 parent 9a2c95b commit 877b982

File tree

9 files changed

+569
-59
lines changed

9 files changed

+569
-59
lines changed

api/envoy/extensions/filters/http/dynamic_forward_proxy/v3/dynamic_forward_proxy.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ message FilterConfig {
4141
// ``envoy.stream.upstream_address`` (See
4242
// :repo:`upstream_address.h<source/common/stream_info/upstream_address.h>`).
4343
bool save_upstream_address = 2;
44+
45+
// When this flag is set, the filter will check for the ``envoy.upstream.dynamic_host``
46+
// and/or ``envoy.upstream.dynamic_port`` filter state values before using the HTTP
47+
// Host header for DNS resolution. This provides consistency with the
48+
// :ref:`SNI dynamic forward proxy <envoy_v3_api_msg_extensions.filters.network.sni_dynamic_forward_proxy.v3.FilterConfig>` and
49+
// :ref:`UDP dynamic forward proxy <envoy_v3_api_msg_extensions.filters.udp.udp_proxy.session.dynamic_forward_proxy.v3.FilterConfig>`
50+
// filters behavior when enabled.
51+
//
52+
// If the flag is not set (default), the filter will use the HTTP Host header
53+
// for DNS resolution, maintaining backward compatibility.
54+
bool allow_dynamic_host_from_filter_state = 4;
4455
}
4556

4657
// Per route Configuration for the dynamic forward proxy HTTP filter.

changelogs/current.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,5 +363,12 @@ new_features:
363363
Added envoy_v3_api_field_extensions.upstreams.http.v3.Http3ProtocolOptions.disable_connection_flow_control_for_streams, an experimental
364364
option for disabling connection level flow control for streams. This is useful in situations where the streams share the same
365365
connection but originate from different end-clients, so that each stream can make progress independently at non-front-line proxies.
366+
- area: dfp
367+
change: |
368+
Added :ref:`allow_dynamic_host_from_filter_state
369+
<envoy_v3_api_field_extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig.allow_dynamic_host_from_filter_state>`
370+
flag to HTTP Dynamic Forward Proxy filter. When enabled, the filter will check for ``envoy.upstream.dynamic_host`` and
371+
``envoy.upstream.dynamic_port`` filter state values before using the HTTP Host header, providing consistency with SNI
372+
and UDP DFP filters. When disabled (default), maintains backward compatibility by using the HTTP Host header directly.
366373
367374
deprecated:

docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,59 @@ To use :ref:`AppleDnsResolverConfig<envoy_v3_api_msg_extensions.network.dns_reso
7575
.. literalinclude:: _include/dns-cache-circuit-breaker-apple.yaml
7676
:language: yaml
7777

78+
Dynamic Host Resolution via Filter State
79+
----------------------------------------
80+
81+
The dynamic forward proxy filter supports dynamic host and port resolution through filter state when the
82+
:ref:`allow_dynamic_host_from_filter_state <envoy_v3_api_field_extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig.allow_dynamic_host_from_filter_state>`
83+
option is enabled. When this feature is enabled, the filter will check for the following filter state values
84+
before falling back to the HTTP Host header:
85+
86+
* ``envoy.upstream.dynamic_host``: Specifies the target host for DNS resolution and connection.
87+
* ``envoy.upstream.dynamic_port``: Specifies the target port for connection.
88+
89+
When the ``allow_dynamic_host_from_filter_state`` flag is enabled, the HTTP Dynamic Forward Proxy
90+
filter will check for filter state values, providing consistency with the SNI and UDP Dynamic Forward
91+
Proxy filters and allowing the same filter state mechanism to work across all proxy types.
92+
93+
Host Resolution Priority
94+
^^^^^^^^^^^^^^^^^^^^^^^^
95+
96+
The filter resolves the target host and port using the following priority order:
97+
98+
When ``allow_dynamic_host_from_filter_state`` is enabled:
99+
100+
1. **Filter State Values:** ``envoy.upstream.dynamic_host`` and ``envoy.upstream.dynamic_port`` from the stream's filter state.
101+
2. **Host Rewrite Configuration:** Values specified in the route's or virtual host's ``typed_per_filter_config``.
102+
3. **HTTP Host Header:** The host and port from the incoming HTTP request's Host/Authority header.
103+
104+
When ``allow_dynamic_host_from_filter_state`` is disabled (default):
105+
106+
1. **Host Rewrite Configuration:** Values specified in the route's or virtual host's ``typed_per_filter_config``.
107+
2. **HTTP Host Header:** The host and port from the incoming HTTP request's Host/Authority header.
108+
109+
Filter State Usage Example
110+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
111+
112+
The filter state values can be set by other filters in the filter chain before the dynamic forward proxy
113+
filter processes the request. For example, using a Set Filter State HTTP filter:
114+
115+
.. code-block:: yaml
116+
117+
http_filters:
118+
- name: envoy.filters.http.set_filter_state
119+
typed_config:
120+
"@type": type.googleapis.com/envoy.extensions.filters.http.set_filter_state.v3.Config
121+
on_request_headers:
122+
- object_key: "envoy.upstream.dynamic_host"
123+
format_string:
124+
text_format_source:
125+
inline_string: "example.com"
126+
- object_key: "envoy.upstream.dynamic_port"
127+
format_string:
128+
text_format_source:
129+
inline_string: "443"
130+
78131
Statistics
79132
----------
80133

source/extensions/clusters/dynamic_forward_proxy/cluster.cc

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -366,36 +366,56 @@ Cluster::LoadBalancer::chooseHost(Upstream::LoadBalancerContext* context) {
366366
return {nullptr};
367367
}
368368

369+
// For host lookup, we need to make sure to match the host of any DNS cache
370+
// insert. Two code points currently do DNS cache insert: the http DFP filter,
371+
// which inserts for HTTP traffic, and sets port based on the cluster's
372+
// security level, and the SNI DFP network filter which sets port based on
373+
// stream metadata, or configuration (which is then added as stream metadata).
374+
const bool is_secure = cluster_.info()
375+
->transportSocketMatcher()
376+
.resolve(nullptr, nullptr)
377+
.factory_.implementsSecureTransport();
378+
const uint32_t default_port = is_secure ? 443 : 80;
379+
380+
const auto* stream_info = context->requestStreamInfo();
369381
const Router::StringAccessor* dynamic_host_filter_state = nullptr;
370-
if (context->requestStreamInfo()) {
371-
dynamic_host_filter_state =
372-
context->requestStreamInfo()->filterState()->getDataReadOnly<Router::StringAccessor>(
373-
DynamicHostFilterStateKey);
382+
if (stream_info) {
383+
dynamic_host_filter_state = stream_info->filterState().getDataReadOnly<Router::StringAccessor>(
384+
DynamicHostFilterStateKey);
374385
}
375386

376387
absl::string_view raw_host;
388+
uint32_t port = default_port;
389+
377390
if (dynamic_host_filter_state) {
391+
// Use dynamic host from filter state if available.
378392
raw_host = dynamic_host_filter_state->asString();
393+
394+
// Try to get port from filter state first.
395+
const StreamInfo::UInt32Accessor* dynamic_port_filter_state =
396+
stream_info->filterState().getDataReadOnly<StreamInfo::UInt32Accessor>(
397+
DynamicPortFilterStateKey);
398+
399+
if (dynamic_port_filter_state != nullptr && dynamic_port_filter_state->value() > 0 &&
400+
dynamic_port_filter_state->value() <= 65535) {
401+
// Use dynamic port from filter state if available.
402+
port = dynamic_port_filter_state->value();
403+
}
404+
// If no dynamic port is in filter state, we just use the default_port.
379405
} else if (context->downstreamHeaders()) {
380406
raw_host = context->downstreamHeaders()->getHostValue();
407+
// When no filter state is used, we let ``normalizeHostForDfp()`` handle the port parsing.
381408
} else if (context->downstreamConnection()) {
382409
raw_host = context->downstreamConnection()->requestedServerName();
383410
}
384411

385-
// For host lookup, we need to make sure to match the host of any DNS cache
386-
// insert. Two code points currently do DNS cache insert: the http DFP filter,
387-
// which inserts for HTTP traffic, and sets port based on the cluster's
388-
// security level, and the SNI DFP network filter which sets port based on
389-
// stream metadata, or configuration (which is then added as stream metadata).
390-
const bool is_secure = cluster_.info()
391-
->transportSocketMatcher()
392-
.resolve(nullptr, nullptr)
393-
.factory_.implementsSecureTransport();
394-
uint32_t port = is_secure ? 443 : 80;
395-
if (context->requestStreamInfo()) {
412+
// We always check for dynamic port from filter state, even if the host is not from filter state.
413+
// This is to maintain the backward compatibility with the existing SNI filter behavior.
414+
if (stream_info && !dynamic_host_filter_state) {
396415
const StreamInfo::UInt32Accessor* dynamic_port_filter_state =
397-
context->requestStreamInfo()->filterState()->getDataReadOnly<StreamInfo::UInt32Accessor>(
416+
stream_info->filterState().getDataReadOnly<StreamInfo::UInt32Accessor>(
398417
DynamicPortFilterStateKey);
418+
399419
if (dynamic_port_filter_state != nullptr && dynamic_port_filter_state->value() > 0 &&
400420
dynamic_port_filter_state->value() <= 65535) {
401421
port = dynamic_port_filter_state->value();
@@ -406,6 +426,7 @@ Cluster::LoadBalancer::chooseHost(Upstream::LoadBalancerContext* context) {
406426
ENVOY_LOG(debug, "host empty");
407427
return {nullptr, "empty_host_header"};
408428
}
429+
409430
std::string hostname =
410431
Common::DynamicForwardProxy::DnsHostInfo::normalizeHostForDfp(raw_host, port);
411432

@@ -422,7 +443,7 @@ Cluster::LoadBalancer::chooseHost(Upstream::LoadBalancerContext* context) {
422443
return {host};
423444
}
424445

425-
// If the host is not found, the DFP cluster cluster can now do asynchronous lookup.
446+
// If the host is not found, the DFP cluster can now do asynchronous lookup.
426447
Upstream::ResourceAutoIncDecPtr handle = cluster_.dns_cache_->canCreateDnsRequest();
427448

428449
// Return an immediate failure if there's too many requests already.

source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "envoy/config/core/v3/base.pb.h"
55
#include "envoy/extensions/clusters/dynamic_forward_proxy/v3/cluster.pb.h"
66
#include "envoy/extensions/filters/http/dynamic_forward_proxy/v3/dynamic_forward_proxy.pb.h"
7+
#include "envoy/router/string_accessor.h"
8+
#include "envoy/stream_info/uint32_accessor.h"
79

810
#include "source/common/http/utility.h"
911
#include "source/common/network/filter_state_proxy_info.h"
@@ -24,7 +26,33 @@ void latchTime(Http::StreamDecoderFilterCallbacks* decoder_callbacks, absl::stri
2426
downstream_timing.setValue(key, decoder_callbacks->dispatcher().timeSource().monotonicTime());
2527
}
2628

29+
// Helper function to apply filter state overrides to host and port.
30+
// Conditionally checks filter state based on the allow_dynamic_host_from_filter_state flag.
31+
void applyFilterStateOverrides(absl::string_view& host, uint32_t& port,
32+
Http::StreamDecoderFilterCallbacks* decoder_callbacks,
33+
bool allow_dynamic_host_from_filter_state) {
34+
if (!allow_dynamic_host_from_filter_state) {
35+
return;
36+
}
37+
38+
const Router::StringAccessor* dynamic_host_filter_state =
39+
decoder_callbacks->streamInfo().filterState()->getDataReadOnly<Router::StringAccessor>(
40+
"envoy.upstream.dynamic_host");
41+
if (dynamic_host_filter_state) {
42+
host = dynamic_host_filter_state->asString();
43+
}
44+
45+
const StreamInfo::UInt32Accessor* dynamic_port_filter_state =
46+
decoder_callbacks->streamInfo().filterState()->getDataReadOnly<StreamInfo::UInt32Accessor>(
47+
"envoy.upstream.dynamic_port");
48+
if (dynamic_port_filter_state != nullptr && dynamic_port_filter_state->value() > 0 &&
49+
dynamic_port_filter_state->value() <= 65535) {
50+
port = dynamic_port_filter_state->value();
51+
}
52+
}
53+
2754
} // namespace
55+
2856
struct ResponseStringValues {
2957
const std::string DnsCacheOverflow = "DNS cache overflow";
3058
const std::string PendingRequestOverflow = "Dynamic forward proxy pending request overflow";
@@ -64,7 +92,8 @@ ProxyFilterConfig::ProxyFilterConfig(
6492
tls_slot_(context.serverFactoryContext().threadLocal()),
6593
cluster_init_timeout_(PROTOBUF_GET_MS_OR_DEFAULT(proto_config.sub_cluster_config(),
6694
cluster_init_timeout, 5000)),
67-
save_upstream_address_(proto_config.save_upstream_address()) {
95+
save_upstream_address_(proto_config.save_upstream_address()),
96+
allow_dynamic_host_from_filter_state_(proto_config.allow_dynamic_host_from_filter_state()) {
6897
tls_slot_.set(
6998
[&](Event::Dispatcher&) { return std::make_shared<ThreadLocalClusterInfo>(*this); });
7099
}
@@ -115,16 +144,15 @@ LoadClusterEntryHandlePtr ProxyFilterConfig::addDynamicCluster(
115144

116145
ProxyFilterConfig::ThreadLocalClusterInfo::~ThreadLocalClusterInfo() {
117146
for (const auto& it : pending_clusters_) {
118-
for (auto cluster : it.second) {
147+
for (const auto cluster : it.second) {
119148
cluster->cancel();
120149
}
121150
}
122151
}
123152
void ProxyFilterConfig::ThreadLocalClusterInfo::onClusterAddOrUpdate(
124153
absl::string_view cluster_name, Upstream::ThreadLocalClusterCommand&) {
125154
ENVOY_LOG(debug, "thread local cluster {} added or updated", cluster_name);
126-
auto it = pending_clusters_.find(cluster_name);
127-
if (it != pending_clusters_.end()) {
155+
if (const auto it = pending_clusters_.find(cluster_name); it != pending_clusters_.end()) {
128156
for (auto* cluster : it->second) {
129157
auto& callbacks = cluster->callbacks_;
130158
cluster->cancel();
@@ -170,8 +198,8 @@ bool ProxyFilter::isProxying() {
170198

171199
Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
172200
Router::RouteConstSharedPtr route = decoder_callbacks_->route();
173-
const Router::RouteEntry* route_entry;
174-
if (!route || !(route_entry = route->routeEntry())) {
201+
const Router::RouteEntry* route_entry = route ? route->routeEntry() : nullptr;
202+
if (!route_entry) {
175203
return Http::FilterHeadersStatus::Continue;
176204
}
177205

@@ -288,20 +316,32 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea
288316
return Http::FilterHeadersStatus::StopIteration;
289317
}
290318
}
291-
auto result = config_->cache().loadDnsCacheEntryWithForceRefresh(
292-
headers.Host()->value().getStringView(), default_port, is_proxying, force_cache_refresh,
293-
*this);
294-
cache_load_handle_ = std::move(result.handle_);
319+
320+
// Get host value from the request headers.
321+
const auto host_attributes =
322+
Http::Utility::parseAuthority(headers.Host()->value().getStringView());
323+
absl::string_view host = host_attributes.host_;
324+
uint16_t port = host_attributes.port_.value_or(default_port);
325+
326+
// Apply filter state overrides for host and port.
327+
uint32_t port_u32 = port;
328+
applyFilterStateOverrides(host, port_u32, decoder_callbacks_,
329+
config_->allowDynamicHostFromFilterState());
330+
port = port_u32;
331+
332+
auto [status_, handle_, host_info_] = config_->cache().loadDnsCacheEntryWithForceRefresh(
333+
host, port, is_proxying, force_cache_refresh, *this);
334+
cache_load_handle_ = std::move(handle_);
295335
if (cache_load_handle_ == nullptr) {
296336
circuit_breaker_.reset();
297337
}
298338

299-
switch (result.status_) {
339+
switch (status_) {
300340
case LoadDnsCacheEntryStatus::InCache: {
301341
ASSERT(cache_load_handle_ == nullptr);
302342
ENVOY_STREAM_LOG(debug, "DNS cache entry already loaded, continuing", *decoder_callbacks_);
303343

304-
auto const& host = result.host_info_;
344+
auto const& host = host_info_;
305345
latchTime(decoder_callbacks_, DNS_END);
306346
if (is_proxying) {
307347
ENVOY_BUG(host.has_value(), "Proxying request but no host entry in DNS cache.");
@@ -330,18 +370,28 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea
330370
PANIC_DUE_TO_CORRUPT_ENUM;
331371
}
332372

333-
Http::FilterHeadersStatus ProxyFilter::loadDynamicCluster(
334-
Extensions::Common::DynamicForwardProxy::DfpClusterSharedPtr cluster,
335-
Http::RequestHeaderMap& headers, uint16_t default_port) {
373+
Http::FilterHeadersStatus
374+
ProxyFilter::loadDynamicCluster(const Common::DynamicForwardProxy::DfpClusterSharedPtr& cluster,
375+
const Http::RequestHeaderMap& headers, uint16_t default_port) {
376+
377+
// Parse host and port from headers.
336378
const auto host_attributes = Http::Utility::parseAuthority(headers.getHostValue());
337379
auto host = std::string(host_attributes.host_);
338380
auto port = host_attributes.port_.value_or(default_port);
339381

382+
// Apply filter state overrides using the helper function.
383+
absl::string_view host_view = host; // Create string_view for the helper.
384+
uint32_t port_u32 = port;
385+
applyFilterStateOverrides(host_view, port_u32, decoder_callbacks_,
386+
config_->allowDynamicHostFromFilterState());
387+
host = std::string(host_view); // Convert back to string.
388+
port = port_u32;
389+
340390
latchTime(decoder_callbacks_, DNS_START);
341391

342392
// cluster name is prefix + host + port
343-
auto cluster_name = "DFPCluster:" + host + ":" + std::to_string(port);
344-
Upstream::ThreadLocalCluster* local_cluster =
393+
const auto cluster_name = "DFPCluster:" + host + ":" + std::to_string(port);
394+
const Upstream::ThreadLocalCluster* local_cluster =
345395
config_->clusterManager().getThreadLocalCluster(cluster_name);
346396
if (local_cluster && cluster->touch(cluster_name)) {
347397
ENVOY_STREAM_LOG(debug, "using the thread local cluster after touch success",
@@ -352,7 +402,7 @@ Http::FilterHeadersStatus ProxyFilter::loadDynamicCluster(
352402

353403
// Still need to add dynamic cluster again even the thread local cluster exists while touch
354404
// failed, that means the cluster is removed in main thread due to ttl reached.
355-
// Otherwise, we may not be able to get the thread local cluster in router.
405+
// Otherwise, we may not be able to get the thread local cluster in the router.
356406

357407
// Create a new cluster & register a callback to tls
358408
cluster_load_handle_ = config_->addDynamicCluster(cluster, cluster_name, host, port, *this);

0 commit comments

Comments
 (0)