xds: implement xDS server validations (#7972)

This commit is contained in:
sanjaypujare 2021-03-16 15:24:04 -07:00 committed by GitHub
parent 71c4ef730e
commit 224247cf02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 367 additions and 41 deletions

View File

@ -106,7 +106,7 @@ final class ClientXdsClient extends AbstractXdsClient {
private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 =
"type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2"
+ ".HttpConnectionManager";
private static final String TYPE_URL_HTTP_CONNECTION_MANAGER =
static final String TYPE_URL_HTTP_CONNECTION_MANAGER =
"type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3"
+ ".HttpConnectionManager";
private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT =

View File

@ -23,12 +23,16 @@ import com.google.protobuf.InvalidProtocolBufferException;
import io.envoyproxy.envoy.config.core.v3.Address;
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
import io.envoyproxy.envoy.config.listener.v3.Filter;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter;
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
import io.grpc.Internal;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
@ -360,14 +364,81 @@ public final class EnvoyServerProtoData {
}
static FilterChain fromEnvoyProtoFilterChain(
io.envoyproxy.envoy.config.listener.v3.FilterChain proto)
io.envoyproxy.envoy.config.listener.v3.FilterChain proto, boolean isDefaultFilterChain)
throws InvalidProtocolBufferException {
if (!isDefaultFilterChain && proto.getFiltersList().isEmpty()) {
throw new IllegalArgumentException(
"filerChain " + proto.getName() + " has to have envoy.http_connection_manager");
}
HashSet<String> uniqueNames = new HashSet<>();
for (Filter filter : proto.getFiltersList()) {
if (!uniqueNames.add(filter.getName())) {
throw new IllegalArgumentException(
"filerChain " + proto.getName() + " has non-unique filter name:" + filter.getName());
}
validateFilter(filter);
}
return new FilterChain(
FilterChainMatch.fromEnvoyProtoFilterChainMatch(proto.getFilterChainMatch()),
getTlsContextFromFilterChain(proto)
);
}
private static void validateFilter(Filter filter)
throws InvalidProtocolBufferException, IllegalArgumentException {
if (!"envoy.http_connection_manager".equals(filter.getName())) {
throw new IllegalArgumentException("filter " + filter.getName() + " not supported.");
}
if (filter.hasConfigDiscovery()) {
throw new IllegalArgumentException(
"filter " + filter.getName() + " with config_discovery not supported");
}
if (!filter.hasTypedConfig()) {
throw new IllegalArgumentException(
"filter " + filter.getName() + " expected to have typed_config");
}
Any any = filter.getTypedConfig();
if (!any.getTypeUrl().equals(ClientXdsClient.TYPE_URL_HTTP_CONNECTION_MANAGER)) {
throw new IllegalArgumentException(
"filter " + filter.getName() + " with unsupported typed_config type:" + any
.getTypeUrl());
}
validateHttpConnectionManager(any.unpack(HttpConnectionManager.class));
}
private static void validateHttpConnectionManager(HttpConnectionManager hcm)
throws IllegalArgumentException {
List<HttpFilter> httpFilters = hcm.getHttpFiltersList();
HashSet<String> uniqueNames = new HashSet<>();
for (HttpFilter httpFilter : httpFilters) {
String httpFilterName = httpFilter.getName();
if (!uniqueNames.add(httpFilterName)) {
throw new IllegalArgumentException(
"http-connection-manager has non-unique http-filter name:" + httpFilterName);
}
if (!httpFilter.getIsOptional()) {
if (!"envoy.router".equals(httpFilterName)) {
throw new IllegalArgumentException(
"http-connection-manager has unsupported http-filter:" + httpFilterName);
}
if (httpFilter.hasConfigDiscovery()) {
throw new IllegalArgumentException(
"http-connection-manager http-filter " + httpFilterName
+ " uses config-discovery which is unsupported");
}
if (httpFilter.hasTypedConfig()) {
Any any = httpFilter.getTypedConfig();
if (!any.getTypeUrl()
.equals("type.googleapis.com/envoy.extensions.filters.http.router.v3.Router")) {
throw new IllegalArgumentException(
"http-connection-manager http-filter " + httpFilterName
+ " has unsupported typed-config type:" + any.getTypeUrl());
}
}
}
}
}
@Nullable
private static DownstreamTlsContext getTlsContextFromFilterChain(
io.envoyproxy.envoy.config.listener.v3.FilterChain filterChain)
@ -459,17 +530,32 @@ public final class EnvoyServerProtoData {
if (!proto.getTrafficDirection().equals(TrafficDirection.INBOUND)) {
throw new IllegalArgumentException("Listener " + proto.getName() + " is not INBOUND");
}
List<FilterChain> filterChains = new ArrayList<>(proto.getFilterChainsCount());
for (io.envoyproxy.envoy.config.listener.v3.FilterChain filterChain :
proto.getFilterChainsList()) {
if (isAcceptable(filterChain.getFilterChainMatch())) {
filterChains.add(FilterChain.fromEnvoyProtoFilterChain(filterChain));
}
if (!proto.getListenerFiltersList().isEmpty()) {
throw new IllegalArgumentException(
"Listener " + proto.getName() + " cannot have listener_filters");
}
if (proto.hasUseOriginalDst()) {
throw new IllegalArgumentException(
"Listener " + proto.getName() + " cannot have use_original_dst set to true");
}
List<FilterChain> filterChains = validateAndSelectFilterChains(proto.getFilterChainsList());
return new Listener(
proto.getName(),
convertEnvoyAddressToString(proto.getAddress()),
filterChains, FilterChain.fromEnvoyProtoFilterChain(proto.getDefaultFilterChain()));
filterChains, FilterChain.fromEnvoyProtoFilterChain(proto.getDefaultFilterChain(), true));
}
private static List<FilterChain> validateAndSelectFilterChains(
List<io.envoyproxy.envoy.config.listener.v3.FilterChain> inputFilterChains)
throws InvalidProtocolBufferException {
List<FilterChain> filterChains = new ArrayList<>(inputFilterChains.size());
for (io.envoyproxy.envoy.config.listener.v3.FilterChain filterChain :
inputFilterChains) {
if (isAcceptable(filterChain.getFilterChainMatch())) {
filterChains.add(FilterChain.fromEnvoyProtoFilterChain(filterChain, false));
}
}
return filterChains;
}
// check if a filter is acceptable for gRPC server side processing

View File

@ -18,16 +18,24 @@ package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
import com.google.protobuf.Any;
import com.google.protobuf.BoolValue;
import com.google.protobuf.UInt32Value;
import com.google.protobuf.util.Durations;
import com.google.re2j.Pattern;
import io.envoyproxy.envoy.config.core.v3.Address;
import io.envoyproxy.envoy.config.core.v3.ExtensionConfigSource;
import io.envoyproxy.envoy.config.core.v3.Locality;
import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent;
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
import io.envoyproxy.envoy.config.core.v3.TransportSocket;
import io.envoyproxy.envoy.config.endpoint.v3.Endpoint;
import io.envoyproxy.envoy.config.listener.v3.Filter;
import io.envoyproxy.envoy.config.listener.v3.FilterChain;
import io.envoyproxy.envoy.config.listener.v3.FilterChainMatch;
import io.envoyproxy.envoy.config.listener.v3.Listener;
import io.envoyproxy.envoy.config.listener.v3.ListenerFilter;
import io.envoyproxy.envoy.config.route.v3.DirectResponseAction;
import io.envoyproxy.envoy.config.route.v3.FilterAction;
import io.envoyproxy.envoy.config.route.v3.RedirectAction;
@ -38,6 +46,9 @@ import io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.QueryParameter
import io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration;
import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.HeaderAbort;
import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter;
import io.envoyproxy.envoy.type.matcher.v3.RegexMatchAndSubstitute;
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher.GoogleRE2;
@ -627,4 +638,256 @@ public class ClientXdsClientDataTest {
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail()).isEqualTo("Listener listener1 is not INBOUND");
}
@Test
public void parseServerSideListener_listenerFiltersPresent() {
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addListenerFilters(ListenerFilter.newBuilder().build())
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("Listener listener1 cannot have listener_filters");
}
@Test
public void parseServerSideListener_useOriginalDst() {
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.setUseOriginalDst(BoolValue.of(true))
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("Listener listener1 cannot have use_original_dst set to true");
}
@Test
public void parseServerSideListener_noHcm() {
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(FilterChain.newBuilder().build())
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("filerChain has to have envoy.http_connection_manager");
}
@Test
public void parseServerSideListener_duplicateFilterName() {
FilterChain filterChain =
buildFilterChain(
Filter.newBuilder()
.setName("envoy.http_connection_manager")
.setTypedConfig(Any.pack(HttpConnectionManager.getDefaultInstance()))
.build(),
Filter.newBuilder()
.setName("envoy.http_connection_manager")
.setTypedConfig(Any.pack(HttpConnectionManager.getDefaultInstance()))
.build());
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("filerChain has non-unique filter name:envoy.http_connection_manager");
}
@Test
public void parseServerSideListener_nonHcmFilter() {
FilterChain filterChain =
buildFilterChain(
Filter.newBuilder()
.setName("xyz")
.setTypedConfig(Any.pack(HttpConnectionManager.getDefaultInstance()))
.build());
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail()).isEqualTo("filter xyz not supported.");
}
@Test
public void parseServerSideListener_configDiscoveryFilter() {
Filter filter =
Filter.newBuilder()
.setName("envoy.http_connection_manager")
.setConfigDiscovery(ExtensionConfigSource.newBuilder().build())
.build();
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("filter envoy.http_connection_manager with config_discovery not supported");
}
@Test
public void parseServerSideListener_expectTypedConfigFilter() {
Filter filter = Filter.newBuilder().setName("envoy.http_connection_manager").build();
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("filter envoy.http_connection_manager expected to have typed_config");
}
@Test
public void parseServerSideListener_wrongTypeUrl() {
Filter filter =
Filter.newBuilder()
.setName("envoy.http_connection_manager")
.setTypedConfig(Any.newBuilder().setTypeUrl("badTypeUrl"))
.build();
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo(
"filter envoy.http_connection_manager with unsupported typed_config type:badTypeUrl");
}
@Test
public void parseServerSideListener_duplicateHttpFilter() {
Filter filter =
buildHttpConnectionManager(
"envoy.http_connection_manager",
HttpFilter.newBuilder().setName("hf").setIsOptional(true).build(),
HttpFilter.newBuilder().setName("hf").setIsOptional(true).build());
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("http-connection-manager has non-unique http-filter name:hf");
}
@Test
public void parseServerSideListener_unsupportedHttpFilter() {
Filter filter =
buildHttpConnectionManager(
"envoy.http_connection_manager", HttpFilter.newBuilder().setName("hf").build());
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo("http-connection-manager has unsupported http-filter:hf");
}
@Test
public void parseServerSideListener_configDiscoveryHttpFilter() {
Filter filter =
buildHttpConnectionManager(
"envoy.http_connection_manager",
HttpFilter.newBuilder()
.setName("envoy.router")
.setConfigDiscovery(ExtensionConfigSource.newBuilder().build())
.build());
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo(
"http-connection-manager http-filter envoy.router uses "
+ "config-discovery which is unsupported");
}
@Test
public void parseServerSideListener_badTypeUrlHttpFilter() {
HTTPFault fault = HTTPFault.newBuilder().build();
Filter filter =
buildHttpConnectionManager(
"envoy.http_connection_manager",
HttpFilter.newBuilder()
.setName("envoy.router")
.setTypedConfig(Any.pack(fault, "type.googleapis.com"))
.build());
FilterChain filterChain = buildFilterChain(filter);
Listener listener =
Listener.newBuilder()
.setName("listener1")
.setTrafficDirection(TrafficDirection.INBOUND)
.addFilterChains(filterChain)
.build();
StructOrError<io.grpc.xds.EnvoyServerProtoData.Listener> struct =
ClientXdsClient.parseServerSideListener(listener);
assertThat(struct.getErrorDetail())
.isEqualTo(
"http-connection-manager http-filter envoy.router has unsupported typed-config type:"
+ "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault");
}
static Filter buildHttpConnectionManager(String name, HttpFilter... httpFilters) {
return Filter.newBuilder()
.setName(name)
.setTypedConfig(
Any.pack(
HttpConnectionManager.newBuilder()
.addAllHttpFilters(Arrays.asList(httpFilters))
.build(),
"type.googleapis.com"))
.build();
}
static FilterChain buildFilterChain(Filter... filters) {
return FilterChain.newBuilder()
.setFilterChainMatch(
FilterChainMatch.newBuilder()
.addAllApplicationProtocols(Arrays.asList("managed-mtls"))
.build())
.setTransportSocket(TransportSocket.getDefaultInstance())
.addAllFilters(Arrays.asList(filters))
.build();
}
}

View File

@ -100,10 +100,10 @@ public abstract class ClientXdsClientTestBase {
private static final String RDS_RESOURCE = "route-configuration.googleapis.com";
private static final String CDS_RESOURCE = "cluster.googleapis.com";
private static final String EDS_RESOURCE = "cluster-load-assignment.googleapis.com";
private static final String VERSION_1 = "42";
private static final String VERSION_2 = "43";
private static final String LISTENER_RESOURCE =
"grpc/server?xds.resource.listening_address=0.0.0.0:7000";
private static final String VERSION_1 = "42";
private static final String VERSION_2 = "43";
private static final Node NODE = Node.newBuilder().build();
private static final FakeClock.TaskFilter RPC_RETRY_TASK_FILTER =

View File

@ -56,7 +56,6 @@ public class EnvoyServerProtoDataTest {
io.envoyproxy.envoy.config.listener.v3.Listener.newBuilder()
.setName("8000")
.setAddress(address)
.addFilterChains(createOutFilter())
.addFilterChains(createInFilter())
.setDefaultFilterChain(createDefaultFilterChain())
.setTrafficDirection(TrafficDirection.INBOUND)
@ -67,18 +66,9 @@ public class EnvoyServerProtoDataTest {
assertThat(xdsListener.getAddress()).isEqualTo("10.2.1.34:8000");
List<EnvoyServerProtoData.FilterChain> filterChains = xdsListener.getFilterChains();
assertThat(filterChains).isNotNull();
assertThat(filterChains.size()).isEqualTo(2);
EnvoyServerProtoData.FilterChain outFilter = filterChains.get(0);
assertThat(outFilter).isNotNull();
EnvoyServerProtoData.FilterChainMatch outFilterChainMatch = outFilter.getFilterChainMatch();
assertThat(outFilterChainMatch).isNotNull();
assertThat(outFilterChainMatch.getDestinationPort()).isEqualTo(8000);
assertThat(outFilterChainMatch.getApplicationProtocols()).isEmpty();
assertThat(outFilterChainMatch.getPrefixRanges()).isEmpty();
assertThat(outFilter.getDownstreamTlsContext())
.isNull();
assertThat(filterChains.size()).isEqualTo(1);
EnvoyServerProtoData.FilterChain inFilter = filterChains.get(1);
EnvoyServerProtoData.FilterChain inFilter = filterChains.get(0);
assertThat(inFilter).isNotNull();
EnvoyServerProtoData.FilterChainMatch inFilterChainMatch = inFilter.getFilterChainMatch();
assertThat(inFilterChainMatch).isNotNull();
@ -109,20 +99,6 @@ public class EnvoyServerProtoDataTest {
.containsExactly(new EnvoyServerProtoData.CidrRange("10.20.0.16", 30));
}
private static FilterChain createOutFilter() {
FilterChain filterChain =
FilterChain.newBuilder()
.setFilterChainMatch(
FilterChainMatch.newBuilder()
.setDestinationPort(UInt32Value.of(8000))
.build())
.addFilters(Filter.newBuilder()
.setName("envoy.http_connection_manager")
.build())
.build();
return filterChain;
}
private static FilterChain createInFilter() {
FilterChain filterChain =
FilterChain.newBuilder()
@ -152,8 +128,9 @@ public class EnvoyServerProtoDataTest {
.setName("envoy.http_connection_manager")
.setTypedConfig(Any.newBuilder()
.setTypeUrl(
"type.googleapis.com/envoy.config.filter.network.http_connection_manager"
+ ".v2.HttpConnectionManager"))
"type.googleapis.com/"
+ "envoy.extensions.filters.network.http_connection_manager"
+ ".v3.HttpConnectionManager"))
.build())
.build();
return filterChain;
@ -186,8 +163,8 @@ public class EnvoyServerProtoDataTest {
Any.newBuilder()
.setTypeUrl(
"type.googleapis.com/"
+ "envoy.config.filter.network.http_connection_manager"
+ ".v2.HttpConnectionManager"))
+ "envoy.extensions.filters.network.http_connection_manager"
+ ".v3.HttpConnectionManager"))
.build())
.build();
return filterChain;