From af077caf29ecf91147e40413350181cd5f9055f5 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Thu, 4 Dec 2025 22:59:16 +0530 Subject: [PATCH] geoip: allow extracting the client IP address from a custom request header Signed-off-by: Rohit Agrawal --- .../filters/http/geoip/v3/geoip.proto | 28 +++- changelogs/current.yaml | 5 + .../extensions/filters/http/geoip/config.cc | 20 ++- source/extensions/filters/http/geoip/config.h | 6 +- .../filters/http/geoip/geoip_filter.cc | 23 ++- .../filters/http/geoip/geoip_filter.h | 7 + .../filters/http/geoip/config_test.cc | 66 ++++++++- .../geoip/geoip_filter_integration_test.cc | 81 +++++++++++ .../filters/http/geoip/geoip_filter_test.cc | 132 ++++++++++++++++++ 9 files changed, 354 insertions(+), 14 deletions(-) diff --git a/api/envoy/extensions/filters/http/geoip/v3/geoip.proto b/api/envoy/extensions/filters/http/geoip/v3/geoip.proto index 4ef26a8245e22..a6c2c97aa8a9e 100644 --- a/api/envoy/extensions/filters/http/geoip/v3/geoip.proto +++ b/api/envoy/extensions/filters/http/geoip/v3/geoip.proto @@ -24,18 +24,34 @@ message Geoip { message XffConfig { // The number of additional ingress proxy hops from the right side of the // :ref:`config_http_conn_man_headers_x-forwarded-for` HTTP header to trust when - // determining the origin client's IP address. The default is zero if this option - // is not specified. See the documentation for + // determining the origin client's IP address. See the documentation for // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. + // + // Defaults to ``0``. uint32 xff_num_trusted_hops = 1; } - // If set, the :ref:`xff_num_trusted_hops ` field will be used to determine - // trusted client address from ``x-forwarded-for`` header. - // Otherwise, the immediate downstream connection source address will be used. - // [#next-free-field: 2] + // Configuration for extracting the client IP address from the + // ``x-forwarded-for`` header. If set, the + // :ref:`xff_num_trusted_hops ` + // field will be used to determine the trusted client address from the ``x-forwarded-for`` header. + // If not set, the immediate downstream connection source address will be used. + // + // Only one of ``xff_config`` or + // :ref:`ip_address_header ` + // can be set. XffConfig xff_config = 1; + // If set, the client IP address will be extracted from the specified request header. + // The header value must contain a valid IP address (IPv4 or IPv6). If the header is + // missing or contains an invalid IP address, the filter will fall back to using the + // immediate downstream connection source address. + // + // Only one of ``ip_address_header`` or + // :ref:`xff_config ` + // can be set. + string ip_address_header = 4; + // Geoip driver specific configuration which depends on the driver being instantiated. // See the geoip drivers for examples: // diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 05c15f9632224..75cc5e81f545a 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -444,5 +444,10 @@ new_features: - area: attributes change: | added :ref:`attributes ` for looking up request or response headers bytes. +- area: geoip + change: | + Added :ref:`ip_address_header ` + to allow extracting the client IP address from a custom request header instead of using the + ``x-forwarded-for`` header or downstream connection source address. deprecated: diff --git a/source/extensions/filters/http/geoip/config.cc b/source/extensions/filters/http/geoip/config.cc index 86c83472dc918..77b28b948c08a 100644 --- a/source/extensions/filters/http/geoip/config.cc +++ b/source/extensions/filters/http/geoip/config.cc @@ -11,9 +11,27 @@ namespace Extensions { namespace HttpFilters { namespace Geoip { -Http::FilterFactoryCb GeoipFilterFactory::createFilterFactoryFromProtoTyped( +namespace { +// Validates the GeoIP filter configuration. +absl::Status validateConfig(const envoy::extensions::filters::http::geoip::v3::Geoip& config) { + // xff_config and ip_address_header are mutually exclusive. + if (config.has_xff_config() && !config.ip_address_header().empty()) { + return absl::InvalidArgumentError( + "Only one of xff_config or ip_address_header can be set in the geoip filter configuration"); + } + return absl::OkStatus(); +} +} // namespace + +absl::StatusOr GeoipFilterFactory::createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::geoip::v3::Geoip& proto_config, const std::string& stat_prefix, Server::Configuration::FactoryContext& context) { + // Validate configuration before creating the filter. + auto status = validateConfig(proto_config); + if (!status.ok()) { + return status; + } + GeoipFilterConfigSharedPtr filter_config( std::make_shared(proto_config, stat_prefix, context.scope())); diff --git a/source/extensions/filters/http/geoip/config.h b/source/extensions/filters/http/geoip/config.h index 677cff6be55b9..a5dc0f69c1d02 100644 --- a/source/extensions/filters/http/geoip/config.h +++ b/source/extensions/filters/http/geoip/config.h @@ -15,11 +15,11 @@ namespace Geoip { * Config registration for the geoip filter. @see NamedHttpFilterConfigFactory. */ class GeoipFilterFactory - : public Common::FactoryBase { + : public Common::ExceptionFreeFactoryBase { public: - GeoipFilterFactory() : FactoryBase("envoy.filters.http.geoip") {} + GeoipFilterFactory() : ExceptionFreeFactoryBase("envoy.filters.http.geoip") {} - Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + absl::StatusOr createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::geoip::v3::Geoip& proto_config, const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; }; diff --git a/source/extensions/filters/http/geoip/geoip_filter.cc b/source/extensions/filters/http/geoip/geoip_filter.cc index 462d06a45fbd6..bce109b08d7d0 100644 --- a/source/extensions/filters/http/geoip/geoip_filter.cc +++ b/source/extensions/filters/http/geoip/geoip_filter.cc @@ -3,6 +3,7 @@ #include "envoy/extensions/filters/http/geoip/v3/geoip.pb.h" #include "source/common/http/utility.h" +#include "source/common/network/utility.h" #include "absl/memory/memory.h" @@ -17,7 +18,8 @@ GeoipFilterConfig::GeoipFilterConfig( : scope_(scope), stat_name_set_(scope.symbolTable().makeSet("Geoip")), stats_prefix_(stat_name_set_->add(stat_prefix + "geoip")), use_xff_(config.has_xff_config()), xff_num_trusted_hops_(config.has_xff_config() ? config.xff_config().xff_num_trusted_hops() - : 0) { + : 0), + ip_address_header_(config.ip_address_header()) { stat_name_set_->rememberBuiltin("total"); } @@ -38,11 +40,26 @@ Http::FilterHeadersStatus GeoipFilter::decodeHeaders(Http::RequestHeaderMap& hea request_headers_ = headers; Network::Address::InstanceConstSharedPtr remote_address; - if (config_->useXff() && config_->xffNumTrustedHops() > 0) { + if (config_->useIpAddressHeader()) { + // Extract IP address from the configured custom header. + const auto header_value = headers.get(config_->ipAddressHeader()); + if (!header_value.empty()) { + const std::string ip_string(header_value[0]->value().getStringView()); + remote_address = Network::Utility::parseInternetAddressNoThrow(ip_string); + if (remote_address == nullptr) { + ENVOY_LOG(debug, "Geoip filter: failed to parse IP address from header '{}': '{}'", + config_->ipAddressHeader().get(), ip_string); + } + } else { + ENVOY_LOG(debug, "Geoip filter: configured header '{}' is missing from request", + config_->ipAddressHeader().get()); + } + } else if (config_->useXff() && config_->xffNumTrustedHops() > 0) { remote_address = Envoy::Http::Utility::getLastAddressFromXFF(headers, config_->xffNumTrustedHops()).address_; } - // If `config_->useXff() == false` or xff header has not been populated for some reason. + // Fallback to the downstream connection source address if no other address source is configured + // or if extraction from the configured source failed. if (!remote_address) { remote_address = decoder_callbacks_->streamInfo().downstreamAddressProvider().remoteAddress(); } diff --git a/source/extensions/filters/http/geoip/geoip_filter.h b/source/extensions/filters/http/geoip/geoip_filter.h index bbc2345b311e4..5d0b0295c0a43 100644 --- a/source/extensions/filters/http/geoip/geoip_filter.h +++ b/source/extensions/filters/http/geoip/geoip_filter.h @@ -5,6 +5,7 @@ #include "envoy/extensions/filters/http/geoip/v3/geoip.pb.h" #include "envoy/geoip/geoip_provider_driver.h" #include "envoy/http/filter.h" +#include "envoy/http/header_map.h" #include "envoy/stats/scope.h" namespace Envoy { @@ -25,6 +26,11 @@ class GeoipFilterConfig { bool useXff() const { return use_xff_; } uint32_t xffNumTrustedHops() const { return xff_num_trusted_hops_; } + // Returns true if a custom header should be used to extract the IP address. + bool useIpAddressHeader() const { return !ip_address_header_.get().empty(); } + // Returns the header name to use for extracting the IP address. + const Http::LowerCaseString& ipAddressHeader() const { return ip_address_header_; } + private: void incCounter(Stats::StatName name); @@ -34,6 +40,7 @@ class GeoipFilterConfig { const Stats::StatName unknown_hit_; bool use_xff_; const uint32_t xff_num_trusted_hops_; + const Http::LowerCaseString ip_address_header_; }; using GeoipFilterConfigSharedPtr = std::shared_ptr; diff --git a/test/extensions/filters/http/geoip/config_test.cc b/test/extensions/filters/http/geoip/config_test.cc index c516b2223d45a..3215c22699cb8 100644 --- a/test/extensions/filters/http/geoip/config_test.cc +++ b/test/extensions/filters/http/geoip/config_test.cc @@ -26,6 +26,12 @@ class GeoipFilterPeer { static uint32_t xffNumTrustedHops(const GeoipFilter& filter) { return filter.config_->xffNumTrustedHops(); } + static bool useIpAddressHeader(const GeoipFilter& filter) { + return filter.config_->useIpAddressHeader(); + } + static const Http::LowerCaseString& ipAddressHeader(const GeoipFilter& filter) { + return filter.config_->ipAddressHeader(); + } }; namespace { @@ -51,6 +57,16 @@ MATCHER_P(HasXffNumTrustedHops, expected, "") { return false; } +MATCHER_P(HasIpAddressHeader, expected, "") { + auto filter = std::static_pointer_cast(arg); + if (GeoipFilterPeer::ipAddressHeader(*filter).get() == expected) { + return true; + } + *result_listener << "expected ip_address_header=" << expected << " but was " + << GeoipFilterPeer::ipAddressHeader(*filter).get(); + return false; +} + TEST(GeoipFilterConfigTest, GeoipFilterDefaultValues) { TestScopedRuntime scoped_runtime; DummyGeoipProviderFactory dummy_factory; @@ -132,12 +148,60 @@ TEST(GeoipFilterConfigTest, GeoipFilterConfigUnknownProvider) { NiceMock context; GeoipFilterFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProtoTyped(filter_config, "geoip", context), + factory.createFilterFactoryFromProtoTyped(filter_config, "geoip", context).IgnoreError(), Envoy::EnvoyException, "Didn't find a registered implementation for 'envoy.geoip_providers.unknown' with type URL: " "''"); } +TEST(GeoipFilterConfigTest, GeoipFilterConfigWithIpAddressHeader) { + TestScopedRuntime scoped_runtime; + DummyGeoipProviderFactory dummy_factory; + Registry::InjectFactory registered(dummy_factory); + std::string filter_config_yaml = R"EOF( + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider + )EOF"; + GeoipFilterConfig filter_config; + TestUtility::loadFromYaml(filter_config_yaml, filter_config); + NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()).Times(2); + GeoipFilterFactory factory; + Http::FilterFactoryCb cb = + factory.createFilterFactoryFromProto(filter_config, "geoip", context).value(); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, + addStreamDecoderFilter(AllOf(HasUseXff(false), HasIpAddressHeader("x-real-ip")))); + cb(filter_callback); +} + +TEST(GeoipFilterConfigTest, GeoipFilterConfigMutualExclusionXffAndIpAddressHeader) { + TestScopedRuntime scoped_runtime; + DummyGeoipProviderFactory dummy_factory; + Registry::InjectFactory registered(dummy_factory); + std::string filter_config_yaml = R"EOF( + xff_config: + xff_num_trusted_hops: 1 + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider + )EOF"; + GeoipFilterConfig filter_config; + TestUtility::loadFromYaml(filter_config_yaml, filter_config); + NiceMock context; + GeoipFilterFactory factory; + auto status_or = factory.createFilterFactoryFromProtoTyped(filter_config, "geoip", context); + EXPECT_FALSE(status_or.ok()); + EXPECT_EQ(status_or.status().message(), + "Only one of xff_config or ip_address_header can be set in the geoip filter " + "configuration"); +} + } // namespace } // namespace Geoip } // namespace HttpFilters diff --git a/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc b/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc index 9fa8eb429863c..2d03f5718cdd8 100644 --- a/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc +++ b/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc @@ -115,6 +115,25 @@ const std::string ConfigIsApplePrivateRelayOnly = R"EOF( isp_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoIP2-ISP-Test.mmdb" )EOF"; +const std::string ConfigWithIpAddressHeader = R"EOF( +name: envoy.filters.http.geoip +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.geoip.v3.Geoip + ip_address_header: "x-real-ip" + provider: + name: envoy.geoip_providers.maxmind + typed_config: + "@type": type.googleapis.com/envoy.extensions.geoip_providers.maxmind.v3.MaxMindConfig + common_provider_config: + geo_headers_to_add: + country: "x-geo-country" + region: "x-geo-region" + city: "x-geo-city" + asn: "x-geo-asn" + city_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test.mmdb" + asn_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-ASN-Test.mmdb" +)EOF"; + class GeoipFilterIntegrationTest : public testing::TestWithParam, public HttpIntegrationTest { public: @@ -356,6 +375,68 @@ TEST_P(GeoipFilterIntegrationTest, MetricForDbBuildEpochIsEmitted) { test_server_->gauge("http.config_test.maxmind.city_db.db_build_epoch")->value()); } +TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseIpAddressHeader) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithIpAddressHeader)); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-real-ip", "216.160.83.56"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("WA", headerValue("x-geo-region")); + EXPECT_EQ("US", headerValue("x-geo-country")); + EXPECT_EQ("209", headerValue("x-geo-asn")); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + test_server_->waitForCounterEq("http.config_test.geoip.total", 1); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.total")->value()); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.hit")->value()); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.total")->value()); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.hit")->value()); +} + +TEST_P(GeoipFilterIntegrationTest, GeoDataNotPopulatedWhenIpAddressHeaderMissing) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithIpAddressHeader)); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + // Request without x-real-ip header should fall back to downstream address (localhost). + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + // Localhost IP is not in the database, so no geo headers should be populated. + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-city")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-region")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-country")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-asn")).empty()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + test_server_->waitForCounterEq("http.config_test.geoip.total", 1); +} + +TEST_P(GeoipFilterIntegrationTest, GeoDataNotPopulatedWhenIpAddressHeaderInvalid) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithIpAddressHeader)); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + // Request with invalid IP in x-real-ip header should fall back to downstream address (localhost). + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-real-ip", "not-a-valid-ip"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + // Localhost IP is not in the database, so no geo headers should be populated. + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-city")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-region")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-country")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-asn")).empty()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + test_server_->waitForCounterEq("http.config_test.geoip.total", 1); +} + } // namespace } // namespace Geoip } // namespace HttpFilters diff --git a/test/extensions/filters/http/geoip/geoip_filter_test.cc b/test/extensions/filters/http/geoip/geoip_filter_test.cc index a0efdfb364a63..302098795a9fe 100644 --- a/test/extensions/filters/http/geoip/geoip_filter_test.cc +++ b/test/extensions/filters/http/geoip/geoip_filter_test.cc @@ -295,6 +295,138 @@ TEST_F(GeoipFilterTest, NoCrashIfFilterDestroyedBeforeCallbackCalled) { ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); } +TEST_F(GeoipFilterTest, UseIpAddressHeaderSuccessfulLookup) { + initializeProviderFactory(); + const std::string external_request_yaml = R"EOF( + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider +)EOF"; + initializeFilter(external_request_yaml); + Http::TestRequestHeaderMapImpl request_headers; + request_headers.addCopy("x-real-ip", "5.6.7.8"); + expectStats(); + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + EXPECT_CALL(*dummy_driver_, lookup(_, _)) + .WillRepeatedly(DoAll(SaveArg<0>(&captured_rq_), SaveArg<1>(&captured_cb_), Invoke([this]() { + captured_cb_(Geolocation::LookupResult{{"x-geo-city", "dummy-city"}}); + }))); + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()); + dispatcher_->run(Event::Dispatcher::RunType::Block); + EXPECT_EQ(2, request_headers.size()); + EXPECT_THAT(request_headers, HasExpectedHeader("x-geo-city", "dummy-city")); + // IP address should be extracted from the x-real-ip header, not the downstream address. + EXPECT_EQ("5.6.7.8:0", captured_rq_.remoteAddress()->asString()); + ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); + filter_->onDestroy(); +} + +TEST_F(GeoipFilterTest, UseIpAddressHeaderWithIpv6) { + initializeProviderFactory(); + const std::string external_request_yaml = R"EOF( + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider +)EOF"; + initializeFilter(external_request_yaml); + Http::TestRequestHeaderMapImpl request_headers; + request_headers.addCopy("x-real-ip", "2001:db8::1"); + expectStats(); + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + EXPECT_CALL(*dummy_driver_, lookup(_, _)) + .WillRepeatedly(DoAll(SaveArg<0>(&captured_rq_), SaveArg<1>(&captured_cb_), Invoke([this]() { + captured_cb_(Geolocation::LookupResult{{"x-geo-city", "dummy-city"}}); + }))); + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()); + dispatcher_->run(Event::Dispatcher::RunType::Block); + EXPECT_EQ(2, request_headers.size()); + EXPECT_THAT(request_headers, HasExpectedHeader("x-geo-city", "dummy-city")); + // IPv6 address should be extracted from the x-real-ip header. + EXPECT_EQ("[2001:db8::1]:0", captured_rq_.remoteAddress()->asString()); + ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); + filter_->onDestroy(); +} + +TEST_F(GeoipFilterTest, UseIpAddressHeaderFallbackOnMissingHeader) { + initializeProviderFactory(); + const std::string external_request_yaml = R"EOF( + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider +)EOF"; + initializeFilter(external_request_yaml); + Http::TestRequestHeaderMapImpl request_headers; + // No x-real-ip header in the request. + expectStats(); + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + EXPECT_CALL(*dummy_driver_, lookup(_, _)) + .WillRepeatedly(DoAll(SaveArg<0>(&captured_rq_), SaveArg<1>(&captured_cb_), Invoke([this]() { + captured_cb_(Geolocation::LookupResult{{"x-geo-city", "dummy-city"}}); + }))); + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()); + dispatcher_->run(Event::Dispatcher::RunType::Block); + EXPECT_EQ(1, request_headers.size()); + EXPECT_THAT(request_headers, HasExpectedHeader("x-geo-city", "dummy-city")); + // Should fall back to downstream address when header is missing. + EXPECT_EQ("1.2.3.4:0", captured_rq_.remoteAddress()->asString()); + ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); + filter_->onDestroy(); +} + +TEST_F(GeoipFilterTest, UseIpAddressHeaderFallbackOnInvalidIp) { + initializeProviderFactory(); + const std::string external_request_yaml = R"EOF( + ip_address_header: "x-real-ip" + provider: + name: "envoy.geoip_providers.dummy" + typed_config: + "@type": type.googleapis.com/test.extensions.filters.http.geoip.DummyProvider +)EOF"; + initializeFilter(external_request_yaml); + Http::TestRequestHeaderMapImpl request_headers; + request_headers.addCopy("x-real-ip", "not-a-valid-ip"); + expectStats(); + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + EXPECT_CALL(*dummy_driver_, lookup(_, _)) + .WillRepeatedly(DoAll(SaveArg<0>(&captured_rq_), SaveArg<1>(&captured_cb_), Invoke([this]() { + captured_cb_(Geolocation::LookupResult{{"x-geo-city", "dummy-city"}}); + }))); + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()); + dispatcher_->run(Event::Dispatcher::RunType::Block); + EXPECT_EQ(2, request_headers.size()); + EXPECT_THAT(request_headers, HasExpectedHeader("x-geo-city", "dummy-city")); + // Should fall back to downstream address when IP is invalid. + EXPECT_EQ("1.2.3.4:0", captured_rq_.remoteAddress()->asString()); + ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); + filter_->onDestroy(); +} + } // namespace } // namespace Geoip } // namespace HttpFilters