Skip to content

Commit 0f6380b

Browse files
authored
xds: server side xDS routing and config application (grpc#8318)
Added routing config discovery for HCM in LdsUpdate in XdsServerWrapper. This can be LDS inline or through RDS. Deal with inflight SslContextProviderSupplier resource handling. Discovered routing config is updated to FilterChainSelectorRef. Added routing config data field in FilterChainSelector. Filter chain matching would resulting in setting a new attribute key for server routing config. Filter chain matching logics mostly not changed. Installed ConfigApplyingInterceptor in XdsServerWrapper's delegateBuilder. It fetches server routing config attribute set above. It does routing match and creates server interceptors for the http filters as a result.
1 parent 46d47d5 commit 0f6380b

10 files changed

Lines changed: 1440 additions & 424 deletions

xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.base.Preconditions.checkNotNull;
2020
import static io.grpc.xds.InternalXdsAttributes.ATTR_FILTER_CHAIN_SELECTOR_REF;
21+
import static io.grpc.xds.XdsServerWrapper.ATTR_SERVER_ROUTING_CONFIG;
2122
import static io.grpc.xds.internal.sds.SdsProtocolNegotiators.ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER;
2223

2324
import com.google.common.annotations.VisibleForTesting;
@@ -36,6 +37,7 @@
3637
import io.grpc.xds.EnvoyServerProtoData.FilterChain;
3738
import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch;
3839
import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector;
40+
import io.grpc.xds.XdsServerWrapper.ServerRoutingConfig;
3941
import io.grpc.xds.internal.Matchers.CidrMatcher;
4042
import io.grpc.xds.internal.sds.SslContextProviderSupplier;
4143
import io.netty.channel.ChannelFutureListener;
@@ -50,6 +52,7 @@
5052
import java.util.Collection;
5153
import java.util.Collections;
5254
import java.util.List;
55+
import java.util.Map;
5356
import java.util.concurrent.Executor;
5457
import java.util.concurrent.atomic.AtomicReference;
5558
import java.util.logging.Level;
@@ -101,32 +104,41 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
101104
return;
102105
}
103106
ProtocolNegotiationEvent pne = (ProtocolNegotiationEvent) evt;
104-
Attributes attr = InternalProtocolNegotiationEvent.getAttributes(pne)
105-
.toBuilder().set(ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER,
106-
config.sslContextProviderSupplier).build();
107+
// TODO(zivy): merge into one key and take care of this outer class visibility.
108+
Attributes attr = InternalProtocolNegotiationEvent.getAttributes(pne).toBuilder()
109+
.set(ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER, config.sslContextProviderSupplier)
110+
.set(ATTR_SERVER_ROUTING_CONFIG, config.routingConfig)
111+
.build();
107112
pne = InternalProtocolNegotiationEvent.withAttributes(pne, attr);
108113
ctx.pipeline().replace(this, null, delegate.newHandler(grpcHandler));
109114
ctx.fireUserEventTriggered(pne);
110115
}
111116

112117
static final class FilterChainSelector {
113118
public static final FilterChainSelector NO_FILTER_CHAIN = new FilterChainSelector(
114-
Collections.<FilterChain>emptyList(), null);
115-
116-
private final List<FilterChain> filterChainList;
119+
Collections.<FilterChain, ServerRoutingConfig>emptyMap(), null, null);
120+
private final Map<FilterChain, ServerRoutingConfig> routingConfigs;
117121
@Nullable
118122
private final SslContextProviderSupplier defaultSslContextProviderSupplier;
123+
@Nullable
124+
private final ServerRoutingConfig defaultRoutingConfig;
119125

120-
FilterChainSelector(List<FilterChain> filterChainList,
121-
@Nullable SslContextProviderSupplier defaultSslContextProviderSupplier) {
122-
checkNotNull(filterChainList, "filterChainList");
123-
this.filterChainList = filterChainList;
126+
FilterChainSelector(Map<FilterChain, ServerRoutingConfig> routingConfigs,
127+
@Nullable SslContextProviderSupplier defaultSslContextProviderSupplier,
128+
@Nullable ServerRoutingConfig defaultRoutingConfig) {
129+
this.routingConfigs = checkNotNull(routingConfigs, "routingConfigs");
124130
this.defaultSslContextProviderSupplier = defaultSslContextProviderSupplier;
131+
this.defaultRoutingConfig = defaultRoutingConfig;
132+
}
133+
134+
@VisibleForTesting
135+
Map<FilterChain, ServerRoutingConfig> getRoutingConfigs() {
136+
return routingConfigs;
125137
}
126138

127139
@VisibleForTesting
128-
List<FilterChain> getFilterChains() {
129-
return filterChainList;
140+
ServerRoutingConfig getDefaultRoutingConfig() {
141+
return defaultRoutingConfig;
130142
}
131143

132144
@VisibleForTesting
@@ -138,7 +150,7 @@ SslContextProviderSupplier getDefaultSslContextProviderSupplier() {
138150
* Throws IllegalStateException when no exact one match, and we should close the connection.
139151
*/
140152
SelectedConfig select(InetSocketAddress localAddr, InetSocketAddress remoteAddr) {
141-
Collection<FilterChain> filterChains = new ArrayList<>(filterChainList);
153+
Collection<FilterChain> filterChains = routingConfigs.keySet();
142154
filterChains = filterOnDestinationPort(filterChains);
143155
filterChains = filterOnIpAddress(filterChains, localAddr.getAddress(), true);
144156
filterChains = filterOnServerNames(filterChains);
@@ -155,10 +167,11 @@ SelectedConfig select(InetSocketAddress localAddr, InetSocketAddress remoteAddr)
155167
}
156168
if (filterChains.size() == 1) {
157169
FilterChain selected = Iterables.getOnlyElement(filterChains);
158-
return new SelectedConfig(selected.getSslContextProviderSupplier());
170+
return new SelectedConfig(
171+
routingConfigs.get(selected), selected.getSslContextProviderSupplier());
159172
}
160-
if (defaultSslContextProviderSupplier != null) {
161-
return new SelectedConfig(defaultSslContextProviderSupplier);
173+
if (defaultRoutingConfig != null) {
174+
return new SelectedConfig(defaultRoutingConfig, defaultSslContextProviderSupplier);
162175
}
163176
return null;
164177
}
@@ -361,10 +374,13 @@ public void close() {
361374
* The FilterChain level configuration.
362375
*/
363376
private static final class SelectedConfig {
377+
private final ServerRoutingConfig routingConfig;
364378
@Nullable
365379
private final SslContextProviderSupplier sslContextProviderSupplier;
366380

367-
private SelectedConfig(@Nullable SslContextProviderSupplier sslContextProviderSupplier) {
381+
private SelectedConfig(ServerRoutingConfig routingConfig,
382+
@Nullable SslContextProviderSupplier sslContextProviderSupplier) {
383+
this.routingConfig = checkNotNull(routingConfig, "routingConfig");
368384
this.sslContextProviderSupplier = sslContextProviderSupplier;
369385
}
370386
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright 2021 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.common.base.Joiner;
22+
import io.grpc.Metadata;
23+
import io.grpc.xds.VirtualHost.Route.RouteMatch;
24+
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
25+
import io.grpc.xds.internal.Matchers.FractionMatcher;
26+
import io.grpc.xds.internal.Matchers.HeaderMatcher;
27+
import java.util.List;
28+
import java.util.Locale;
29+
import javax.annotation.Nullable;
30+
31+
/**
32+
* Utilities for performing virtual host domain name matching and route matching.
33+
*/
34+
// TODO(chengyuanzhang): clean up implementations in XdsNameResolver.
35+
final class RoutingUtils {
36+
// Prevent instantiation.
37+
private RoutingUtils() {
38+
}
39+
40+
/**
41+
* Returns the {@link VirtualHost} with the best match domain for the given hostname.
42+
*/
43+
@Nullable
44+
static VirtualHost findVirtualHostForHostName(List<VirtualHost> virtualHosts, String hostName) {
45+
// Domain search order:
46+
// 1. Exact domain names: ``www.foo.com``.
47+
// 2. Suffix domain wildcards: ``*.foo.com`` or ``*-bar.foo.com``.
48+
// 3. Prefix domain wildcards: ``foo.*`` or ``foo-*``.
49+
// 4. Special wildcard ``*`` matching any domain.
50+
//
51+
// The longest wildcards match first.
52+
// Assuming only a single virtual host in the entire route configuration can match
53+
// on ``*`` and a domain must be unique across all virtual hosts.
54+
int matchingLen = -1; // longest length of wildcard pattern that matches host name
55+
boolean exactMatchFound = false; // true if a virtual host with exactly matched domain found
56+
VirtualHost targetVirtualHost = null; // target VirtualHost with longest matched domain
57+
for (VirtualHost vHost : virtualHosts) {
58+
for (String domain : vHost.domains()) {
59+
boolean selected = false;
60+
if (matchHostName(hostName, domain)) { // matching
61+
if (!domain.contains("*")) { // exact matching
62+
exactMatchFound = true;
63+
targetVirtualHost = vHost;
64+
break;
65+
} else if (domain.length() > matchingLen) { // longer matching pattern
66+
selected = true;
67+
} else if (domain.length() == matchingLen && domain.startsWith("*")) { // suffix matching
68+
selected = true;
69+
}
70+
}
71+
if (selected) {
72+
matchingLen = domain.length();
73+
targetVirtualHost = vHost;
74+
}
75+
}
76+
if (exactMatchFound) {
77+
break;
78+
}
79+
}
80+
return targetVirtualHost;
81+
}
82+
83+
/**
84+
* Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with
85+
* case-insensitive.
86+
*
87+
* <p>Wildcard pattern rules:
88+
* <ol>
89+
* <li>A single asterisk (*) matches any domain.</li>
90+
* <li>Asterisk (*) is only permitted in the left-most or the right-most part of the pattern,
91+
* but not both.</li>
92+
* </ol>
93+
*/
94+
private static boolean matchHostName(String hostName, String pattern) {
95+
checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."),
96+
"Invalid host name");
97+
checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."),
98+
"Invalid pattern/domain name");
99+
100+
hostName = hostName.toLowerCase(Locale.US);
101+
pattern = pattern.toLowerCase(Locale.US);
102+
// hostName and pattern are now in lower case -- domain names are case-insensitive.
103+
104+
if (!pattern.contains("*")) {
105+
// Not a wildcard pattern -- hostName and pattern must match exactly.
106+
return hostName.equals(pattern);
107+
}
108+
// Wildcard pattern
109+
110+
if (pattern.length() == 1) {
111+
return true;
112+
}
113+
114+
int index = pattern.indexOf('*');
115+
116+
// At most one asterisk (*) is allowed.
117+
if (pattern.indexOf('*', index + 1) != -1) {
118+
return false;
119+
}
120+
121+
// Asterisk can only match prefix or suffix.
122+
if (index != 0 && index != pattern.length() - 1) {
123+
return false;
124+
}
125+
126+
// HostName must be at least as long as the pattern because asterisk has to
127+
// match one or more characters.
128+
if (hostName.length() < pattern.length()) {
129+
return false;
130+
}
131+
132+
if (index == 0 && hostName.endsWith(pattern.substring(1))) {
133+
// Prefix matching fails.
134+
return true;
135+
}
136+
137+
// Pattern matches hostname if suffix matching succeeds.
138+
return index == pattern.length() - 1
139+
&& hostName.startsWith(pattern.substring(0, pattern.length() - 1));
140+
}
141+
142+
/**
143+
* Returns {@code true} iff the given {@link RouteMatch} matches the RPC's full method name and
144+
* headers.
145+
*/
146+
static boolean matchRoute(RouteMatch routeMatch, String fullMethodName,
147+
Metadata headers, ThreadSafeRandom random) {
148+
if (!matchPath(routeMatch.pathMatcher(), fullMethodName)) {
149+
return false;
150+
}
151+
for (HeaderMatcher headerMatcher : routeMatch.headerMatchers()) {
152+
if (!matchHeader(headerMatcher, getHeaderValue(headers, headerMatcher.name()))) {
153+
return false;
154+
}
155+
}
156+
FractionMatcher fraction = routeMatch.fractionMatcher();
157+
return fraction == null || random.nextInt(fraction.denominator()) < fraction.numerator();
158+
}
159+
160+
private static boolean matchPath(PathMatcher pathMatcher, String fullMethodName) {
161+
if (pathMatcher.path() != null) {
162+
return pathMatcher.caseSensitive()
163+
? pathMatcher.path().equals(fullMethodName)
164+
: pathMatcher.path().equalsIgnoreCase(fullMethodName);
165+
} else if (pathMatcher.prefix() != null) {
166+
return pathMatcher.caseSensitive()
167+
? fullMethodName.startsWith(pathMatcher.prefix())
168+
: fullMethodName.toLowerCase().startsWith(pathMatcher.prefix().toLowerCase());
169+
}
170+
return pathMatcher.regEx().matches(fullMethodName);
171+
}
172+
173+
private static boolean matchHeader(HeaderMatcher headerMatcher, @Nullable String value) {
174+
if (headerMatcher.present() != null) {
175+
return (value == null) == headerMatcher.present().equals(headerMatcher.inverted());
176+
}
177+
if (value == null) {
178+
return false;
179+
}
180+
boolean baseMatch;
181+
if (headerMatcher.exactValue() != null) {
182+
baseMatch = headerMatcher.exactValue().equals(value);
183+
} else if (headerMatcher.safeRegEx() != null) {
184+
baseMatch = headerMatcher.safeRegEx().matches(value);
185+
} else if (headerMatcher.range() != null) {
186+
long numValue;
187+
try {
188+
numValue = Long.parseLong(value);
189+
baseMatch = numValue >= headerMatcher.range().start()
190+
&& numValue <= headerMatcher.range().end();
191+
} catch (NumberFormatException ignored) {
192+
baseMatch = false;
193+
}
194+
} else if (headerMatcher.prefix() != null) {
195+
baseMatch = value.startsWith(headerMatcher.prefix());
196+
} else {
197+
baseMatch = value.endsWith(headerMatcher.suffix());
198+
}
199+
return baseMatch != headerMatcher.inverted();
200+
}
201+
202+
@Nullable
203+
private static String getHeaderValue(Metadata headers, String headerName) {
204+
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
205+
return null;
206+
}
207+
if (headerName.equals("content-type")) {
208+
return "application/grpc";
209+
}
210+
Metadata.Key<String> key;
211+
try {
212+
key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
213+
} catch (IllegalArgumentException e) {
214+
return null;
215+
}
216+
Iterable<String> values = headers.getAll(key);
217+
return values == null ? null : Joiner.on(",").join(values);
218+
}
219+
}

xds/src/main/java/io/grpc/xds/XdsServerBuilder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public final class XdsServerBuilder extends ForwardingServerBuilder<XdsServerBui
4949
private final int port;
5050
private XdsServingStatusListener xdsServingStatusListener;
5151
private AtomicBoolean isServerBuilt = new AtomicBoolean(false);
52+
private final FilterRegistry filterRegistry = FilterRegistry.getDefaultRegistry();
5253
private XdsClientPoolFactory xdsClientPoolFactory =
5354
SharedXdsClientPoolProvider.getDefaultProvider();
5455

@@ -98,7 +99,7 @@ public Server build() {
9899
.set(ATTR_FILTER_CHAIN_SELECTOR_REF, filterChainSelectorRef)
99100
.build());
100101
return new XdsServerWrapper("0.0.0.0:" + port, delegate, xdsServingStatusListener,
101-
filterChainSelectorRef, xdsClientPoolFactory);
102+
filterChainSelectorRef, xdsClientPoolFactory, filterRegistry);
102103
}
103104

104105
@VisibleForTesting

0 commit comments

Comments
 (0)