xds/federation: validate and canonify resource name

On reading a new `xdstp`: resource name, do a validation on the URI and canonify the query params.
This commit is contained in:
ZHANG Dapeng 2022-01-24 16:55:43 -08:00 committed by GitHub
parent 1231ce686e
commit b29c3ec021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 246 additions and 15 deletions

View File

@ -298,7 +298,12 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res
errors.add("LDS response Resource index " + i + " - can't decode Listener: " + e); errors.add("LDS response Resource index " + i + " - can't decode Listener: " + e);
continue; continue;
} }
String listenerName = listener.getName(); if (!isResourceNameValid(listener.getName(), resource.getTypeUrl())) {
errors.add(
"Unsupported resource name: " + listener.getName() + " for type: " + ResourceType.LDS);
continue;
}
String listenerName = canonifyResourceName(listener.getName());
unpackedResources.add(listenerName); unpackedResources.add(listenerName);
// Process Listener into LdsUpdate. // Process Listener into LdsUpdate.
@ -1425,7 +1430,13 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res
errors.add("RDS response Resource index " + i + " - can't decode RouteConfiguration: " + e); errors.add("RDS response Resource index " + i + " - can't decode RouteConfiguration: " + e);
continue; continue;
} }
String routeConfigName = routeConfig.getName(); if (!isResourceNameValid(routeConfig.getName(), resource.getTypeUrl())) {
errors.add(
"Unsupported resource name: " + routeConfig.getName() + " for type: "
+ ResourceType.RDS);
continue;
}
String routeConfigName = canonifyResourceName(routeConfig.getName());
unpackedResources.add(routeConfigName); unpackedResources.add(routeConfigName);
// Process RouteConfiguration into RdsUpdate. // Process RouteConfiguration into RdsUpdate.
@ -1547,7 +1558,12 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res
errors.add("CDS response Resource index " + i + " - can't decode Cluster: " + e); errors.add("CDS response Resource index " + i + " - can't decode Cluster: " + e);
continue; continue;
} }
String clusterName = cluster.getName(); if (!isResourceNameValid(cluster.getName(), resource.getTypeUrl())) {
errors.add(
"Unsupported resource name: " + cluster.getName() + " for type: " + ResourceType.CDS);
continue;
}
String clusterName = canonifyResourceName(cluster.getName());
// Management server is required to always send newly requested resources, even if they // Management server is required to always send newly requested resources, even if they
// may have been sent previously (proactively). Thus, client does not need to cache // may have been sent previously (proactively). Thus, client does not need to cache
@ -1793,7 +1809,12 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res
"EDS response Resource index " + i + " - can't decode ClusterLoadAssignment: " + e); "EDS response Resource index " + i + " - can't decode ClusterLoadAssignment: " + e);
continue; continue;
} }
String clusterName = assignment.getClusterName(); if (!isResourceNameValid(assignment.getClusterName(), resource.getTypeUrl())) {
errors.add("Unsupported resource name: " + assignment.getClusterName() + " for type: "
+ ResourceType.EDS);
continue;
}
String clusterName = canonifyResourceName(assignment.getClusterName());
// Skip information for clusters not requested. // Skip information for clusters not requested.
// Management server is required to always send newly requested resources, even if they // Management server is required to always send newly requested resources, even if they
@ -2382,10 +2403,6 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res
ResourceSubscriber(ResourceType type, String resource) { ResourceSubscriber(ResourceType type, String resource) {
syncContext.throwIfNotInThisSynchronizationContext(); syncContext.throwIfNotInThisSynchronizationContext();
this.type = type; this.type = type;
// TODO(zdapeng): Validate authority in resource URI for new-style resource name
// when parsing XDS response.
// TODO(zdapeng): Canonicalize the resource name by sorting the context params in normal
// lexicographic order.
this.resource = resource; this.resource = resource;
this.serverInfo = getServerInfo(resource); this.serverInfo = getServerInfo(resource);
// Initialize metadata in UNKNOWN state to cover the case when resource subscriber, // Initialize metadata in UNKNOWN state to cover the case when resource subscriber,

View File

@ -17,9 +17,12 @@
package io.grpc.xds; package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME;
import com.google.auto.value.AutoValue; import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.Any; import com.google.protobuf.Any;
@ -32,6 +35,8 @@ import io.grpc.xds.EnvoyServerProtoData.Listener;
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterDropStats;
import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -49,6 +54,58 @@ import javax.annotation.Nullable;
*/ */
abstract class XdsClient { abstract class XdsClient {
static boolean isResourceNameValid(String resourceName, String typeUrl) {
checkNotNull(resourceName, "resourceName");
if (!resourceName.startsWith(XDSTP_SCHEME)) {
return true;
}
URI uri;
try {
uri = new URI(resourceName);
} catch (URISyntaxException e) {
return false;
}
String path = uri.getPath();
// path must be in the form of /{resource type}/{id/*}
Splitter slashSplitter = Splitter.on('/').omitEmptyStrings();
if (path == null) {
return false;
}
List<String> pathSegs = slashSplitter.splitToList(path);
if (pathSegs.size() < 2) {
return false;
}
String type = pathSegs.get(0);
if (!type.equals(slashSplitter.splitToList(typeUrl).get(1))) {
return false;
}
return true;
}
static String canonifyResourceName(String resourceName) {
checkNotNull(resourceName, "resourceName");
if (!resourceName.startsWith(XDSTP_SCHEME)) {
return resourceName;
}
URI uri = URI.create(resourceName);
String rawQuery = uri.getRawQuery();
Splitter ampSplitter = Splitter.on('&').omitEmptyStrings();
if (rawQuery == null) {
return resourceName;
}
List<String> queries = ampSplitter.splitToList(rawQuery);
if (queries.size() < 2) {
return resourceName;
}
List<String> canonicalContextParams = new ArrayList<>(queries.size());
for (String query : queries) {
canonicalContextParams.add(query);
}
Collections.sort(canonicalContextParams);
String canonifiedQuery = Joiner.on('&').join(canonicalContextParams);
return resourceName.replace(rawQuery, canonifiedQuery);
}
@AutoValue @AutoValue
abstract static class LdsUpdate implements ResourceUpdate { abstract static class LdsUpdate implements ResourceUpdate {
// Http level api listener configuration. // Http level api listener configuration.

View File

@ -49,6 +49,7 @@ import io.grpc.Status.Code;
import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext;
import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ObjectPool; import io.grpc.internal.ObjectPool;
import io.grpc.xds.AbstractXdsClient.ResourceType;
import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.AuthorityInfo;
import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.BootstrapInfo;
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig; import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
@ -195,6 +196,13 @@ final class XdsNameResolver extends NameResolver {
replacement = percentEncodePath(replacement); replacement = percentEncodePath(replacement);
} }
String ldsResourceName = expandPercentS(listenerNameTemplate, replacement); String ldsResourceName = expandPercentS(listenerNameTemplate, replacement);
if (!XdsClient.isResourceNameValid(ldsResourceName, ResourceType.LDS.typeUrl())
&& !XdsClient.isResourceNameValid(ldsResourceName, ResourceType.LDS.typeUrlV2())) {
listener.onError(Status.INVALID_ARGUMENT.withDescription(
"invalid listener resource URI for service authority: " + serviceAuthority));
return;
}
ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName);
callCounterProvider = SharedCallCounterMap.getInstance(); callCounterProvider = SharedCallCounterMap.getInstance();
resolveState = new ResolveState(ldsResourceName); resolveState = new ResolveState(ldsResourceName);
resolveState.start(); resolveState.start();

View File

@ -111,6 +111,7 @@ import io.grpc.lookup.v1.GrpcKeyBuilder.Name;
import io.grpc.lookup.v1.NameMatcher; import io.grpc.lookup.v1.NameMatcher;
import io.grpc.lookup.v1.RouteLookupClusterSpecifier; import io.grpc.lookup.v1.RouteLookupClusterSpecifier;
import io.grpc.lookup.v1.RouteLookupConfig; import io.grpc.lookup.v1.RouteLookupConfig;
import io.grpc.xds.AbstractXdsClient.ResourceType;
import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.ClientXdsClient.ResourceInvalidException; import io.grpc.xds.ClientXdsClient.ResourceInvalidException;
import io.grpc.xds.ClientXdsClient.StructOrError; import io.grpc.xds.ClientXdsClient.StructOrError;
@ -2573,6 +2574,51 @@ public class ClientXdsClientDataTest {
ClientXdsClient.validateUpstreamTlsContext(upstreamTlsContext, null); ClientXdsClient.validateUpstreamTlsContext(upstreamTlsContext, null);
} }
@Test
public void validateResourceName() {
String traditionalResource = "cluster1.google.com";
assertThat(XdsClient.isResourceNameValid(traditionalResource, ResourceType.CDS.typeUrl()))
.isTrue();
assertThat(XdsClient.isResourceNameValid(traditionalResource, ResourceType.RDS.typeUrlV2()))
.isTrue();
String invalidPath = "xdstp:/abc/efg";
assertThat(XdsClient.isResourceNameValid(invalidPath, ResourceType.CDS.typeUrl())).isFalse();
String invalidPath2 = "xdstp:///envoy.config.route.v3.RouteConfiguration";
assertThat(XdsClient.isResourceNameValid(invalidPath2, ResourceType.RDS.typeUrl())).isFalse();
String typeMatch = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1";
assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.LDS.typeUrl())).isFalse();
assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.RDS.typeUrl())).isTrue();
assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.RDS.typeUrlV2())).isFalse();
}
@Test
public void canonifyResourceName() {
String traditionalResource = "cluster1.google.com";
assertThat(XdsClient.canonifyResourceName(traditionalResource))
.isEqualTo(traditionalResource);
assertThat(XdsClient.canonifyResourceName(traditionalResource))
.isEqualTo(traditionalResource);
assertThat(XdsClient.canonifyResourceName(traditionalResource))
.isEqualTo(traditionalResource);
assertThat(XdsClient.canonifyResourceName(traditionalResource))
.isEqualTo(traditionalResource);
String withNoQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1";
assertThat(XdsClient.canonifyResourceName(withNoQueries)).isEqualTo(withNoQueries);
String withOneQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1?name=foo";
assertThat(XdsClient.canonifyResourceName(withOneQueries)).isEqualTo(withOneQueries);
String withTwoQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/id/route1?b=1&a=1";
String expectedCanonifiedName =
"xdstp:///envoy.config.route.v3.RouteConfiguration/id/route1?a=1&b=1";
assertThat(XdsClient.canonifyResourceName(withTwoQueries))
.isEqualTo(expectedCanonifiedName);
}
private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters) { private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters) {
return Filter.newBuilder() return Filter.newBuilder()
.setName("envoy.http_connection_manager") .setName("envoy.http_connection_manager")

View File

@ -760,8 +760,9 @@ public abstract class ClientXdsClientTestBase {
@Test @Test
public void ldsResourceUpdated_withXdstpResourceName() { public void ldsResourceUpdated_withXdstpResourceName() {
String ldsResourceName = String ldsResourceName = useProtocolV3()
"xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1"; ? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1"
: "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1";
DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull(); assertThat(channelForCustomAuthority).isNotNull();
verifyResourceMetadataRequested(LDS, ldsResourceName); verifyResourceMetadataRequested(LDS, ldsResourceName);
@ -779,8 +780,9 @@ public abstract class ClientXdsClientTestBase {
@Test @Test
public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() { public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() {
String ldsResourceName = String ldsResourceName = useProtocolV3()
"xdstp:///envoy.config.listener.v3.Listener/listener1"; ? "xdstp:///envoy.config.listener.v3.Listener/listener1"
: "xdstp:///envoy.api.v2.Listener/listener1";
DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher);
assertThat(channelForEmptyAuthority).isNotNull(); assertThat(channelForEmptyAuthority).isNotNull();
verifyResourceMetadataRequested(LDS, ldsResourceName); verifyResourceMetadataRequested(LDS, ldsResourceName);
@ -796,6 +798,107 @@ public abstract class ClientXdsClientTestBase {
LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT);
} }
@Test
public void ldsResourceUpdated_withXdstpResourceName_witUnorderedContextParams() {
String ldsResourceName = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1/a?bar=2&foo=1"
: "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1/a?bar=2&foo=1";
DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull();
String ldsResourceNameWithUnorderedContextParams = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1/a?foo=1&bar=2"
: "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1/a?foo=1&bar=2";
Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(
ldsResourceNameWithUnorderedContextParams,
mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE))));
call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000");
call.verifyRequest(
LDS, ldsResourceName, VERSION_1, "0000", NODE);
}
@Test
public void ldsResourceUpdated_withXdstpResourceName_withWrongType() {
String ldsResourceName = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1"
: "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1";
DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull();
String ldsResourceNameWithWrongType =
"xdstp://authority.xds.com/envoy.config.route.v3.RouteConfiguration/listener1";
Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(
ldsResourceNameWithWrongType,
mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE))));
call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000");
call.verifyRequestNack(
LDS, ldsResourceName, "", "0000", NODE,
ImmutableList.of(
"Unsupported resource name: " + ldsResourceNameWithWrongType + " for type: LDS"));
}
@Test
public void rdsResourceUpdated_withXdstpResourceName_withWrongType() {
String rdsResourceName = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.route.v3.RouteConfiguration/route1"
: "xdstp://authority.xds.com/envoy.api.v2.RouteConfiguration/route1";
DiscoveryRpcCall call = startResourceWatcher(RDS, rdsResourceName, rdsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull();
String rdsResourceNameWithWrongType =
"xdstp://authority.xds.com/envoy.config.listener.v3.Listener/route1";
Any testRouteConfig = Any.pack(mf.buildRouteConfiguration(
rdsResourceNameWithWrongType, mf.buildOpaqueVirtualHosts(VHOST_SIZE)));
call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000");
call.verifyRequestNack(
RDS, rdsResourceName, "", "0000", NODE,
ImmutableList.of(
"Unsupported resource name: " + rdsResourceNameWithWrongType + " for type: RDS"));
}
@Test
public void cdsResourceUpdated_withXdstpResourceName_withWrongType() {
String cdsResourceName = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.cluster.v3.Cluster/cluster1"
: "xdstp://authority.xds.com/envoy.api.v2.Cluster/cluster1";
DiscoveryRpcCall call = startResourceWatcher(CDS, cdsResourceName, cdsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull();
String cdsResourceNameWithWrongType =
"xdstp://authority.xds.com/envoy.config.listener.v3.Listener/cluster1";
Any testClusterConfig = Any.pack(mf.buildEdsCluster(
cdsResourceNameWithWrongType, null, "round_robin", null, null, false, null,
"envoy.transport_sockets.tls", null));
call.sendResponse(CDS, testClusterConfig, VERSION_1, "0000");
call.verifyRequestNack(
CDS, cdsResourceName, "", "0000", NODE,
ImmutableList.of(
"Unsupported resource name: " + cdsResourceNameWithWrongType + " for type: CDS"));
}
@Test
public void edsResourceUpdated_withXdstpResourceName_withWrongType() {
String edsResourceName = useProtocolV3()
? "xdstp://authority.xds.com/envoy.config.endpoint.v3.ClusterLoadAssignment/cluster1"
: "xdstp://authority.xds.com/envoy.api.v2.ClusterLoadAssignment/cluster1";
DiscoveryRpcCall call = startResourceWatcher(EDS, edsResourceName, edsResourceWatcher);
assertThat(channelForCustomAuthority).isNotNull();
String edsResourceNameWithWrongType =
"xdstp://authority.xds.com/envoy.config.listener.v3.Listener/cluster1";
Any testEdsConfig = Any.pack(mf.buildClusterLoadAssignment(
edsResourceNameWithWrongType,
ImmutableList.of(mf.buildLocalityLbEndpoints(
"region2", "zone2", "subzone2",
mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 0)),
ImmutableList.of()));
call.sendResponse(EDS, testEdsConfig, VERSION_1, "0000");
call.verifyRequestNack(
EDS, edsResourceName, "", "0000", NODE,
ImmutableList.of(
"Unsupported resource name: " + edsResourceNameWithWrongType + " for type: EDS"));
}
@Test @Test
public void ldsResourceUpdate_withFaultInjection() { public void ldsResourceUpdate_withFaultInjection() {
Assume.assumeTrue(useProtocolV3()); Assume.assumeTrue(useProtocolV3());

View File

@ -270,12 +270,12 @@ public class XdsNameResolverTest {
.node(Node.newBuilder().build()) .node(Node.newBuilder().build())
.authorities( .authorities(
ImmutableMap.of(targetAuthority, AuthorityInfo.create( ImmutableMap.of(targetAuthority, AuthorityInfo.create(
"xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s", "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2",
ImmutableList.<ServerInfo>of(ServerInfo.create( ImmutableList.<ServerInfo>of(ServerInfo.create(
"td.googleapis.com", InsecureChannelCredentials.create(), true))))) "td.googleapis.com", InsecureChannelCredentials.create(), true)))))
.build(); .build();
expectedLdsResourceName = expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/"
"xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%5B::FFFF:129.144.52.38%5D:80"; + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified
resolver = new XdsNameResolver( resolver = new XdsNameResolver(
"xds.authority.com", serviceAuthority, serviceConfigParser, syncContext, scheduler, "xds.authority.com", serviceAuthority, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null);