Skip to content

Commit e6f6f9d

Browse files
committed
Introduce host simplification rules
Today, Envoy only supports a single wildcard ("*") in virtual host domain entries, which must be either a prefix or suffix. This limitation greatly simplifies the virtual host matching logic, but it is also inherently limiting. This change introduces a repeated list of "host simplification rules" at the RouteConfiguration level, that provide a way to substitute away other dynamic portions of the domain without changing what is sent upstream. For example, to match something like `*.foo.*.example.org` you might write a simplification rule like: `([^.]+[.]foo[.])([^.]+)([.]example[.]org)` with a substitution of `\1bar\3`. This then allows a virtual host domain entry of `*.foo.bar.example.org` to match `baz.foo.bar.example.org` or `wowza.foo.qux.example.org`. Host simplification rules are processed in the order they are defined. Signed-off-by: Ryan Anderson <[email protected]>
1 parent d812698 commit e6f6f9d

File tree

5 files changed

+100
-0
lines changed

5 files changed

+100
-0
lines changed

api/envoy/config/route/v3/route.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package envoy.config.route.v3;
55
import "envoy/config/core/v3/base.proto";
66
import "envoy/config/core/v3/config_source.proto";
77
import "envoy/config/route/v3/route_components.proto";
8+
import "envoy/type/matcher/v3/regex.proto";
89

910
import "google/protobuf/any.proto";
1011
import "google/protobuf/wrappers.proto";
@@ -155,6 +156,14 @@ message RouteConfiguration {
155156
// For instance, if the metadata is intended for the Router filter,
156157
// the filter name should be specified as ``envoy.filters.http.router``.
157158
core.v3.Metadata metadata = 17;
159+
160+
// The host simplification rules are a set of regex substitutions
161+
// that can modify the :authority used when matching
162+
// VirtualHosts. It will not change what is sent upstream. This can
163+
// be used to implement multiple-wildcard matching, by converting
164+
// all but one of the wildcards into a static string.
165+
// This is similar to ignore_port_in_host_matching (above), but more flexible.
166+
repeated type.matcher.v3.RegexMatchAndSubstitute host_simplification_rules = 18;
158167
}
159168

160169
message Vhds {

changelogs/current.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,5 +359,10 @@ new_features:
359359
Added a new metric ``db_build_epoch`` to track the build timestamp of the MaxMind geolocation database files.
360360
This can be used to monitor the freshness of the databases currently in use by the filter.
361361
See `MaxMind DB build_epoch <https://maxmind.github.io/MaxMind-DB/#build_epoch>`_ for more details.
362+
- area: http
363+
change: |
364+
Added support for :ref:`host_simplification_rules <envoy_v3_api_field_config.route.v3.RouteConfiguration.host_simplification_rules>` to allow for
365+
regular expression substitutions to "simplify" a host before doing
366+
virtual host matching.
362367
363368
deprecated:

source/common/router/config_impl.cc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,6 +1929,17 @@ RouteMatcher::RouteMatcher(const envoy::config::route::v3::RouteConfiguration& r
19291929
}
19301930
}
19311931
}
1932+
for (const auto& simplification_rule : route_config.host_simplification_rules()) {
1933+
auto result =
1934+
Regex::Utility::parseRegex(simplification_rule.pattern(), factory_context.regexEngine());
1935+
1936+
SET_AND_RETURN_IF_NOT_OK(result.status(), creation_status);
1937+
1938+
std::unique_ptr<SimplificationRule> rule = std::make_unique<SimplificationRule>(
1939+
std::move(*result), simplification_rule.substitution());
1940+
1941+
host_simplification_rules_.push_back(std::move(rule));
1942+
}
19321943
}
19331944

19341945
const VirtualHostImpl* RouteMatcher::findVirtualHost(const Http::RequestHeaderMap& headers) const {
@@ -1952,6 +1963,16 @@ const VirtualHostImpl* RouteMatcher::findVirtualHost(const Http::RequestHeaderMa
19521963
host_header_value = host_header_value.substr(0, port_start);
19531964
}
19541965
}
1966+
1967+
// If any host simplification rules exist, process them in order to
1968+
// rewrite the host header used when looking up virtual hosts. (This
1969+
// is notionally similar to the handling of
1970+
// `ignore_port_in_host_matching`, but more flexible.)
1971+
for (const auto& simplifier : host_simplification_rules_) {
1972+
host_header_value =
1973+
simplifier->matcher->replaceAll(host_header_value, simplifier->substitution);
1974+
}
1975+
19551976
// TODO (@rshriram) Match Origin header in WebSocket
19561977
// request with VHost, using wildcard match
19571978
// Lower-case the value of the host header, as hostnames are case insensitive.

source/common/router/config_impl.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <string>
1010
#include <vector>
1111

12+
#include "envoy/config/common/matcher/v3/matcher.pb.h"
1213
#include "envoy/config/core/v3/base.pb.h"
1314
#include "envoy/config/route/v3/route.pb.h"
1415
#include "envoy/config/route/v3/route_components.pb.h"
@@ -1255,6 +1256,12 @@ class RouteListMatchActionFactory : public Matcher::ActionFactory<RouteActionCon
12551256

12561257
DECLARE_FACTORY(RouteListMatchActionFactory);
12571258

1259+
// Helper structure to keep the matcher and substitution together.
1260+
struct SimplificationRule {
1261+
const Regex::CompiledMatcherPtr matcher;
1262+
const std::string substitution;
1263+
};
1264+
12581265
/**
12591266
* Wraps the route configuration which matches an incoming request headers to a backend cluster.
12601267
* This is split out mainly to help with unit testing.
@@ -1303,6 +1310,8 @@ class RouteMatcher {
13031310

13041311
VirtualHostImplSharedPtr default_virtual_host_;
13051312
const bool ignore_port_in_host_matching_{false};
1313+
1314+
std::vector<std::unique_ptr<SimplificationRule>> host_simplification_rules_;
13061315
};
13071316

13081317
/**

test/common/router/config_impl_test.cc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,62 @@ TEST_F(RouteMatcherTest, IgnorePortInHostMatching) {
24952495
}
24962496
}
24972497

2498+
// Tests that host_simplification_rules mutate/simplify the host used
2499+
// for picking the virtualhost when matching
2500+
TEST_F(RouteMatcherTest, HostSimplificationRules) {
2501+
const std::string yaml = R"EOF(
2502+
host_simplification_rules:
2503+
- pattern:
2504+
regex: "^(foo[.])([^.]+)([.]example[.]org)$"
2505+
substitution: \1bar\3
2506+
virtual_hosts:
2507+
- name: local_service
2508+
domains: ["foo.bar.example.org"]
2509+
routes:
2510+
- match:
2511+
prefix: ""
2512+
name: "business-specific-route"
2513+
route:
2514+
cluster: local_service_grpc
2515+
- name: catchall_host
2516+
domains:
2517+
- "*"
2518+
routes:
2519+
- match:
2520+
prefix: ""
2521+
name: "default-route"
2522+
route:
2523+
cluster: default_catch_all_service
2524+
)EOF";
2525+
auto route_configuration = parseRouteConfigurationFromYaml(yaml);
2526+
2527+
factory_context_.cluster_manager_.initializeClusters(
2528+
{"local_service_grpc", "default_catch_all_service"}, {});
2529+
{
2530+
TestConfigImpl config(route_configuration, factory_context_, true, creation_status_);
2531+
// First, the trivial, no substitution needed, but should happen anyway:
2532+
EXPECT_EQ(config.route(genHeaders("foo.bar.example.org", "/foo", "GET"), 0)->routeName(),
2533+
"business-specific-route");
2534+
// Matches, but requires the substitution to happen:
2535+
EXPECT_EQ(config.route(genHeaders("foo.baz.example.org", "/foo", "GET"), 0)->routeName(),
2536+
"business-specific-route");
2537+
// Matches, require substitution, longer replaceable section
2538+
EXPECT_EQ(
2539+
config.route(genHeaders("foo.barbazquxfoobang.example.org", "/foo", "GET"), 0)->routeName(),
2540+
"business-specific-route");
2541+
// Shouldn't match, but has a related substring:
2542+
EXPECT_EQ(config.route(genHeaders("qux.foo.baz.example.org", "/foo", "GET"), 0)->routeName(),
2543+
"default-route");
2544+
// Shouldn't match (trivial)
2545+
EXPECT_EQ(config.route(genHeaders("12.34.56.78:1234", "/foo", "GET"), 0)->routeName(),
2546+
"default-route");
2547+
EXPECT_EQ(config.route(genHeaders("www.foo.com:8090", "/foo", "GET"), 0)->routeName(),
2548+
"default-route");
2549+
EXPECT_EQ(config.route(genHeaders("[12:34:56:7890::]:8090", "/foo", "GET"), 0)->routeName(),
2550+
"default-route");
2551+
}
2552+
}
2553+
24982554
TEST_F(RouteMatcherTest, Priority) {
24992555
const std::string yaml = R"EOF(
25002556
virtual_hosts:

0 commit comments

Comments
 (0)