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