diff --git a/CODEOWNERS b/CODEOWNERS index 3209be2307338..1f4bc2abbfde6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -196,3 +196,5 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp /*/extensions/filters/http/set_metadata @aguinet @snowp # Formatters /*/extensions/formatter/req_without_query @dio @tsaarni +# IP address input matcher +/*/extensions/matching/input_matchers/ip @aguinet @snowp diff --git a/api/BUILD b/api/BUILD index 6a7c33c66eccf..c23b494d0f792 100644 --- a/api/BUILD +++ b/api/BUILD @@ -211,6 +211,7 @@ proto_library( "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", "//envoy/extensions/matching/common_inputs/environment_variable/v3:pkg", "//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg", + "//envoy/extensions/matching/input_matchers/ip/v3:pkg", "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/quic/crypto_stream/v3:pkg", "//envoy/extensions/quic/proof_source/v3:pkg", diff --git a/api/envoy/extensions/matching/input_matchers/ip/v3/BUILD b/api/envoy/extensions/matching/input_matchers/ip/v3/BUILD new file mode 100644 index 0000000000000..1c1a6f6b44235 --- /dev/null +++ b/api/envoy/extensions/matching/input_matchers/ip/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/matching/input_matchers/ip/v3/ip.proto b/api/envoy/extensions/matching/input_matchers/ip/v3/ip.proto new file mode 100644 index 0000000000000..3c7cb4eb5f19a --- /dev/null +++ b/api/envoy/extensions/matching/input_matchers/ip/v3/ip.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.matching.input_matchers.ip.v3; + +import "envoy/config/core/v3/address.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.matching.input_matchers.ip.v3"; +option java_outer_classname = "IpProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: IP matcher] +// [#extension: envoy.matching.input_matchers.ip] + +// This input matcher matches IPv4 or IPv6 addresses against a list of CIDR +// ranges. It returns true if and only if the input IP belongs to at least one +// of these CIDR ranges. Internally, it uses a Level-Compressed trie, as +// described in the paper `IP-address lookup using LC-tries +// `_ +// by S. Nilsson and G. Karlsson. For "big" lists of IPs, this matcher is more +// efficient than multiple single IP matcher, that would have a linear cost. +message Ip { + // Match if the IP belongs to any of these CIDR ranges. + repeated config.core.v3.CidrRange cidr_ranges = 1 [(validate.rules).repeated = {min_items: 1}]; + + // The human readable prefix to use when emitting statistics for the IP input + // matcher. Names in the table below are concatenated to this prefix. + // + // .. csv-table:: + // :header: Name, Type, Description + // :widths: 1, 1, 2 + // + // ip_parsing_failed, Counter, Total number of IP addresses the matcher was unable to parse + string stat_prefix = 2 [(validate.rules).string = {min_len: 1}]; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index eb4267263f3f2..51bc63183a1c4 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -140,6 +140,7 @@ proto_library( "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", "//envoy/extensions/matching/common_inputs/environment_variable/v3:pkg", "//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg", + "//envoy/extensions/matching/input_matchers/ip/v3:pkg", "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/quic/crypto_stream/v3:pkg", "//envoy/extensions/quic/proof_source/v3:pkg", diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index 2826b6c67ecdb..ea123c074ca1e 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -27,4 +27,5 @@ Common messages ../extensions/filters/common/dependency/v3/dependency.proto ../extensions/filters/common/matcher/action/v3/skip_action.proto ../extensions/matching/input_matchers/consistent_hashing/v3/consistent_hashing.proto + ../extensions/matching/input_matchers/ip/v3/ip.proto ../extensions/matching/common_inputs/environment_variable/v3/input.proto diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 2b4e64afe0fc7..5f3fc61ed01bd 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -100,6 +100,7 @@ New Features * http: added a new option to upstream HTTP/2 :ref:`keepalive ` to send a PING ahead of a new stream if the connection has been idle for a sufficient duration. * http: added the ability to :ref:`unescape slash sequences` in the path. Requests with unescaped slashes can be proxied, rejected or redirected to the new unescaped path. By default this feature is disabled. The default behavior can be overridden through :ref:`http_connection_manager.path_with_escaped_slashes_action` runtime variable. This action can be selectively enabled for a portion of requests by setting the :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling` runtime variable. * http: added upstream and downstream alpha HTTP/3 support! See :ref:`quic_options ` for downstream and the new http3_protocol_options in :ref:`http_protocol_options ` for upstream HTTP/3. +* input matcher: a new input matcher that :ref:`matches an IP address against a list of CIDR ranges `. * jwt_authn: added support to fetch remote jwks asynchronously specified by :ref:`async_fetch `. * listener: added ability to change an existing listener's address. * local_rate_limit_filter: added suppoort for locally rate limiting http requests on a per connection basis. This can be enabled by setting the :ref:`local_rate_limit_per_downstream_connection ` field to true. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 6a7c33c66eccf..c23b494d0f792 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -211,6 +211,7 @@ proto_library( "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", "//envoy/extensions/matching/common_inputs/environment_variable/v3:pkg", "//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg", + "//envoy/extensions/matching/input_matchers/ip/v3:pkg", "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/quic/crypto_stream/v3:pkg", "//envoy/extensions/quic/proof_source/v3:pkg", diff --git a/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/BUILD b/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/BUILD new file mode 100644 index 0000000000000..1c1a6f6b44235 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/ip.proto b/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/ip.proto new file mode 100644 index 0000000000000..3c7cb4eb5f19a --- /dev/null +++ b/generated_api_shadow/envoy/extensions/matching/input_matchers/ip/v3/ip.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.matching.input_matchers.ip.v3; + +import "envoy/config/core/v3/address.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.matching.input_matchers.ip.v3"; +option java_outer_classname = "IpProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: IP matcher] +// [#extension: envoy.matching.input_matchers.ip] + +// This input matcher matches IPv4 or IPv6 addresses against a list of CIDR +// ranges. It returns true if and only if the input IP belongs to at least one +// of these CIDR ranges. Internally, it uses a Level-Compressed trie, as +// described in the paper `IP-address lookup using LC-tries +// `_ +// by S. Nilsson and G. Karlsson. For "big" lists of IPs, this matcher is more +// efficient than multiple single IP matcher, that would have a linear cost. +message Ip { + // Match if the IP belongs to any of these CIDR ranges. + repeated config.core.v3.CidrRange cidr_ranges = 1 [(validate.rules).repeated = {min_items: 1}]; + + // The human readable prefix to use when emitting statistics for the IP input + // matcher. Names in the table below are concatenated to this prefix. + // + // .. csv-table:: + // :header: Name, Type, Description + // :widths: 1, 1, 2 + // + // ip_parsing_failed, Counter, Total number of IP addresses the matcher was unable to parse + string stat_prefix = 2 [(validate.rules).string = {min_len: 1}]; +} diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index bfe74f721c120..8ee2ef00e95e7 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -52,6 +52,7 @@ EXTENSIONS = { # "envoy.matching.input_matchers.consistent_hashing": "//source/extensions/matching/input_matchers/consistent_hashing:config", + "envoy.matching.input_matchers.ip": "//source/extensions/matching/input_matchers/ip:config", # # Generic Inputs diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 261b50d1d3c9e..31cdfad79124c 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -499,6 +499,11 @@ envoy.matching.input_matchers.consistent_hashing: - envoy.matching.input_matchers security_posture: robust_to_untrusted_downstream status: stable +envoy.matching.input_matchers.ip: + categories: + - envoy.matching.input_matchers + security_posture: robust_to_untrusted_downstream_and_upstream + status: stable envoy.quic.proof_source.filter_chain: categories: - envoy.quic.proof_source diff --git a/source/extensions/matching/input_matchers/ip/BUILD b/source/extensions/matching/input_matchers/ip/BUILD new file mode 100644 index 0000000000000..fe2104b899c43 --- /dev/null +++ b/source/extensions/matching/input_matchers/ip/BUILD @@ -0,0 +1,33 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "ip_lib", + srcs = ["matcher.cc"], + hdrs = ["matcher.h"], + deps = [ + "//envoy/matcher:matcher_interface", + "//source/common/network:lc_trie_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":ip_lib", + "//envoy/matcher:matcher_interface", + "//envoy/registry", + "//envoy/server:factory_context_interface", + "@envoy_api//envoy/extensions/matching/input_matchers/ip/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/matching/input_matchers/ip/config.cc b/source/extensions/matching/input_matchers/ip/config.cc new file mode 100644 index 0000000000000..8798dd4a007fe --- /dev/null +++ b/source/extensions/matching/input_matchers/ip/config.cc @@ -0,0 +1,47 @@ +#include "source/extensions/matching/input_matchers/ip/config.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +Envoy::Matcher::InputMatcherFactoryCb +Config::createInputMatcherFactoryCb(const Protobuf::Message& config, + Server::Configuration::FactoryContext& context) { + const auto& ip_config = MessageUtil::downcastAndValidate< + const envoy::extensions::matching::input_matchers::ip::v3::Ip&>( + config, context.messageValidationVisitor()); + + const auto& cidr_ranges = ip_config.cidr_ranges(); + std::vector ranges; + ranges.reserve(cidr_ranges.size()); + for (const auto& cidr_range : cidr_ranges) { + const std::string& address = cidr_range.address_prefix(); + const uint32_t prefix_len = cidr_range.prefix_len().value(); + const auto range = Network::Address::CidrRange::create(address, prefix_len); + // We only assert that the range is valid because: + // * if "address" can't be parsed, it will throw an EnvoyException + // * prefix_len can't be < 0 as per the protobuf definition as an uint32_t + // * if prefix_len is too big, CidrRange::create clamps it to a valid value + // => it is thus not possible to create an invalid range. + ASSERT(range.isValid(), "address range should be valid!"); + ranges.emplace_back(std::move(range)); + } + + const std::string& stat_prefix = ip_config.stat_prefix(); + Stats::Scope& scope = context.scope(); + return [ranges, stat_prefix, &scope]() { + return std::make_unique(ranges, stat_prefix, scope); + }; +} +/** + * Static registration for the consistent hashing matcher. @see RegisterFactory. + */ +REGISTER_FACTORY(Config, Envoy::Matcher::InputMatcherFactory); + +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/matching/input_matchers/ip/config.h b/source/extensions/matching/input_matchers/ip/config.h new file mode 100644 index 0000000000000..ade3ecbfae1f1 --- /dev/null +++ b/source/extensions/matching/input_matchers/ip/config.h @@ -0,0 +1,33 @@ +#pragma once + +#include "envoy/extensions/matching/input_matchers/ip/v3/ip.pb.h" +#include "envoy/extensions/matching/input_matchers/ip/v3/ip.pb.validate.h" +#include "envoy/matcher/matcher.h" +#include "envoy/server/factory_context.h" + +#include "source/common/protobuf/utility.h" +#include "source/extensions/matching/input_matchers/ip/matcher.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +class Config : public Envoy::Matcher::InputMatcherFactory { +public: + Envoy::Matcher::InputMatcherFactoryCb + createInputMatcherFactoryCb(const Protobuf::Message& config, + Server::Configuration::FactoryContext& factory_context) override; + + std::string name() const override { return "envoy.matching.matchers.ip"; } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/matching/input_matchers/ip/matcher.cc b/source/extensions/matching/input_matchers/ip/matcher.cc new file mode 100644 index 0000000000000..00db60a8dd628 --- /dev/null +++ b/source/extensions/matching/input_matchers/ip/matcher.cc @@ -0,0 +1,49 @@ +#include "source/extensions/matching/input_matchers/ip/matcher.h" + +#include "source/common/network/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +namespace { + +MatcherStats generateStats(absl::string_view prefix, Stats::Scope& scope) { + return MatcherStats{IP_MATCHER_STATS(POOL_COUNTER_PREFIX(scope, prefix))}; +} + +} // namespace + +Matcher::Matcher(std::vector const& ranges, + absl::string_view stat_prefix, + Stats::Scope& stat_scope) + : // We could put "false" instead of "true". What matters is that the IP + // belongs to the trie. We could further optimize the storage of LcTrie in + // this case by implementing an LcTrie specialization that doesn't + // store any associated data. + trie_({{true, ranges}}), stats_(generateStats(stat_prefix, stat_scope)) {} + +bool Matcher::match(absl::optional input) { + if (!input) { + return false; + } + const absl::string_view ip_str = *input; + if (ip_str.empty()) { + return false; + } + const auto ip = Network::Utility::parseInternetAddressNoThrow(std::string{ip_str}); + if (!ip) { + stats_.ip_parsing_failed_.inc(); + ENVOY_LOG(debug, "IP matcher: unable to parse address '{}'", ip_str); + return false; + } + return !trie_.getData(ip).empty(); +} + +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/matching/input_matchers/ip/matcher.h b/source/extensions/matching/input_matchers/ip/matcher.h new file mode 100644 index 0000000000000..ba035ad579ffb --- /dev/null +++ b/source/extensions/matching/input_matchers/ip/matcher.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "envoy/matcher/matcher.h" +#include "envoy/network/address.h" +#include "envoy/stats/stats_macros.h" + +#include "source/common/network/lc_trie.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +#define IP_MATCHER_STATS(COUNTER) COUNTER(ip_parsing_failed) + +struct MatcherStats { + IP_MATCHER_STATS(GENERATE_COUNTER_STRUCT); +}; + +class Matcher : public Envoy::Matcher::InputMatcher, Logger::Loggable { +public: + Matcher(std::vector const& ranges, absl::string_view stat_prefix, + Stats::Scope& stat_scope); + bool match(absl::optional input) override; + absl::optional stats() const { return stats_; } + +private: + const Network::LcTrie::LcTrie trie_; + MatcherStats stats_; +}; + +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/matching/input_matchers/ip/BUILD b/test/extensions/matching/input_matchers/ip/BUILD new file mode 100644 index 0000000000000..ac9afab392a0f --- /dev/null +++ b/test/extensions/matching/input_matchers/ip/BUILD @@ -0,0 +1,31 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.matching.input_matchers.ip", + deps = [ + "//source/extensions/matching/input_matchers/ip:config", + "//test/mocks/server:factory_context_mocks", + ], +) + +envoy_extension_cc_test( + name = "matcher_test", + srcs = ["matcher_test.cc"], + extension_name = "envoy.matching.input_matchers.ip", + deps = [ + "//source/extensions/matching/input_matchers/ip:ip_lib", + ], +) diff --git a/test/extensions/matching/input_matchers/ip/config_test.cc b/test/extensions/matching/input_matchers/ip/config_test.cc new file mode 100644 index 0000000000000..82f8428622af8 --- /dev/null +++ b/test/extensions/matching/input_matchers/ip/config_test.cc @@ -0,0 +1,85 @@ +#include "source/extensions/matching/input_matchers/ip/config.h" + +#include "test/mocks/server/factory_context.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +TEST(ConfigTest, TestConfig) { + NiceMock context; + + const std::string yaml_string = R"EOF( + name: ip + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidr_ranges: + - address_prefix: 192.0.2.0 + prefix_len: 24 + stat_prefix: "test.ips_matcher" +)EOF"; + + envoy::config::core::v3::TypedExtensionConfig config; + TestUtility::loadFromYaml(yaml_string, config); + + Config factory; + auto message = Envoy::Config::Utility::translateAnyToFactoryConfig( + config.typed_config(), ProtobufMessage::getStrictValidationVisitor(), factory); + auto matcher = factory.createInputMatcherFactoryCb(*message, context); + EXPECT_NE(nullptr, matcher()); +} + +TEST(ConfigTest, InvalidConfigIP) { + NiceMock context; + + const std::string yaml_string = R"EOF( + name: ip + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidr_ranges: + - address_prefix: foo + prefix_len: 10 + stat_prefix: "test.ips_matcher" +)EOF"; + + envoy::config::core::v3::TypedExtensionConfig config; + TestUtility::loadFromYaml(yaml_string, config); + + Config factory; + auto message = Envoy::Config::Utility::translateAnyToFactoryConfig( + config.typed_config(), ProtobufMessage::getStrictValidationVisitor(), factory); + EXPECT_THROW_WITH_MESSAGE(factory.createInputMatcherFactoryCb(*message, context), EnvoyException, + "malformed IP address: foo"); +} + +TEST(ConfigTest, InvalidConfigStats) { + NiceMock context; + + const std::string yaml_string = R"EOF( + name: ip + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidr_ranges: + - address_prefix: 192.0.2.0 + prefix_len: 10 +)EOF"; + + envoy::config::core::v3::TypedExtensionConfig config; + TestUtility::loadFromYaml(yaml_string, config); + + Config factory; + auto message = Envoy::Config::Utility::translateAnyToFactoryConfig( + config.typed_config(), ProtobufMessage::getStrictValidationVisitor(), factory); + EXPECT_THROW_WITH_REGEX(factory.createInputMatcherFactoryCb(*message, context), EnvoyException, + "Proto constraint validation failed.*StatPrefix"); +} + +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/matching/input_matchers/ip/matcher_test.cc b/test/extensions/matching/input_matchers/ip/matcher_test.cc new file mode 100644 index 0000000000000..cd91c92d90337 --- /dev/null +++ b/test/extensions/matching/input_matchers/ip/matcher_test.cc @@ -0,0 +1,87 @@ +#include "envoy/network/address.h" + +#include "source/extensions/matching/input_matchers/ip/matcher.h" + +#include "test/common/stats/stat_test_utility.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace InputMatchers { +namespace IP { + +class MatcherTest : public testing::Test { +public: + void initialize(std::vector&& ranges) { + m_ = std::make_unique(std::move(ranges), stat_prefix_, scope_); + } + + Stats::TestUtil::TestStore scope_; + std::string stat_prefix_{"ipmatcher.test"}; + std::unique_ptr m_; +}; + +TEST_F(MatcherTest, TestV4) { + std::vector ranges; + ranges.emplace_back(Network::Address::CidrRange::create("192.0.2.0", 24)); + ranges.emplace_back(Network::Address::CidrRange::create("10.0.0.0", 24)); + initialize(std::move(ranges)); + EXPECT_FALSE(m_->match("192.0.1.255")); + EXPECT_TRUE(m_->match("192.0.2.0")); + EXPECT_TRUE(m_->match("192.0.2.1")); + EXPECT_TRUE(m_->match("192.0.2.255")); + EXPECT_FALSE(m_->match("9.255.255.255")); + EXPECT_TRUE(m_->match("10.0.0.0")); + EXPECT_TRUE(m_->match("10.0.0.255")); + EXPECT_FALSE(m_->match("10.0.1.0")); +} + +TEST_F(MatcherTest, TestV6) { + std::vector ranges; + ranges.emplace_back(Network::Address::CidrRange::create("::1/128")); + ranges.emplace_back(Network::Address::CidrRange::create("2001::/16")); + ranges.emplace_back(Network::Address::CidrRange::create("2002::/16")); + initialize(std::move(ranges)); + + EXPECT_FALSE(m_->match("::")); + EXPECT_TRUE(m_->match("::1")); + EXPECT_FALSE(m_->match("::2")); + + EXPECT_FALSE(m_->match("2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + EXPECT_TRUE(m_->match("2001::1")); + EXPECT_TRUE(m_->match("2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + EXPECT_TRUE(m_->match("2002::1")); + EXPECT_TRUE(m_->match("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + EXPECT_FALSE(m_->match("2003::")); +} + +TEST_F(MatcherTest, EmptyRanges) { + initialize(std::vector{}); + EXPECT_FALSE(m_->match("192.0.2.0")); +} + +TEST_F(MatcherTest, EmptyIP) { + std::vector ranges; + ranges.emplace_back(Network::Address::CidrRange::create("192.0.2.0", 24)); + initialize(std::move(ranges)); + EXPECT_FALSE(m_->match("")); + EXPECT_FALSE(m_->match(absl::optional{})); +} + +TEST_F(MatcherTest, InvalidIP) { + std::vector ranges; + ranges.emplace_back(Network::Address::CidrRange::create("192.0.2.0", 24)); + initialize(std::move(ranges)); + EXPECT_EQ(m_->stats()->ip_parsing_failed_.value(), 0); + EXPECT_FALSE(m_->match("foo")); + EXPECT_EQ(m_->stats()->ip_parsing_failed_.value(), 1); +} + +} // namespace IP +} // namespace InputMatchers +} // namespace Matching +} // namespace Extensions +} // namespace Envoy