diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 40c473a70e..5823bc85f8 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -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 = diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 330867c6e4..6a95a75710 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -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 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 httpFilters = hcm.getHttpFiltersList(); + HashSet 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 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 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 validateAndSelectFilterChains( + List inputFilterChains) + throws InvalidProtocolBufferException { + List 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 diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java index 08adfdde9b..fe7a4d7caf 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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(); + } } diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index 8535a064a7..fc292a7c29 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -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 = diff --git a/xds/src/test/java/io/grpc/xds/EnvoyServerProtoDataTest.java b/xds/src/test/java/io/grpc/xds/EnvoyServerProtoDataTest.java index e725ca9f2e..6ac70356a8 100644 --- a/xds/src/test/java/io/grpc/xds/EnvoyServerProtoDataTest.java +++ b/xds/src/test/java/io/grpc/xds/EnvoyServerProtoDataTest.java @@ -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 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;