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
28 changes: 22 additions & 6 deletions api/envoy/extensions/filters/http/geoip/v3/geoip.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 <envoy_v3_api_field_extensions.filters.http.geoip.v3.Geoip.XffConfig.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 <envoy_v3_api_field_extensions.filters.http.geoip.v3.Geoip.XffConfig.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 <envoy_v3_api_field_extensions.filters.http.geoip.v3.Geoip.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 <envoy_v3_api_field_extensions.filters.http.geoip.v3.Geoip.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:
//
Expand Down
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,10 @@ new_features:
- area: attributes
change: |
added :ref:`attributes <arch_overview_attributes>` for looking up request or response headers bytes.
- area: geoip
change: |
Added :ref:`ip_address_header <envoy_v3_api_field_extensions.filters.http.geoip.v3.Geoip.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:
20 changes: 19 additions & 1 deletion source/extensions/filters/http/geoip/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::FilterFactoryCb> 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<GeoipFilterConfig>(proto_config, stat_prefix, context.scope()));

Expand Down
6 changes: 3 additions & 3 deletions source/extensions/filters/http/geoip/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ namespace Geoip {
* Config registration for the geoip filter. @see NamedHttpFilterConfigFactory.
*/
class GeoipFilterFactory
: public Common::FactoryBase<envoy::extensions::filters::http::geoip::v3::Geoip> {
: public Common::ExceptionFreeFactoryBase<envoy::extensions::filters::http::geoip::v3::Geoip> {
public:
GeoipFilterFactory() : FactoryBase("envoy.filters.http.geoip") {}
GeoipFilterFactory() : ExceptionFreeFactoryBase("envoy.filters.http.geoip") {}

Http::FilterFactoryCb createFilterFactoryFromProtoTyped(
absl::StatusOr<Http::FilterFactoryCb> createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::geoip::v3::Geoip& proto_config,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override;
};
Expand Down
23 changes: 20 additions & 3 deletions source/extensions/filters/http/geoip/geoip_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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");
}

Expand All @@ -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();
}
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/filters/http/geoip/geoip_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);

Expand All @@ -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<GeoipFilterConfig>;
Expand Down
66 changes: 65 additions & 1 deletion test/extensions/filters/http/geoip/config_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -51,6 +57,16 @@ MATCHER_P(HasXffNumTrustedHops, expected, "") {
return false;
}

MATCHER_P(HasIpAddressHeader, expected, "") {
auto filter = std::static_pointer_cast<GeoipFilter>(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;
Expand Down Expand Up @@ -132,12 +148,60 @@ TEST(GeoipFilterConfigTest, GeoipFilterConfigUnknownProvider) {
NiceMock<Server::Configuration::MockFactoryContext> 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<Geolocation::GeoipProviderFactory> 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<Server::Configuration::MockFactoryContext> 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<Geolocation::GeoipProviderFactory> 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<Server::Configuration::MockFactoryContext> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Network::Address::IpVersion>,
public HttpIntegrationTest {
public:
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading