diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index b14d3404712..cbc67a006a3 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -85,6 +85,7 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -286,15 +287,17 @@ static EnvoyServerProtoData.Listener parseServerSideListener( } List filterChains = new ArrayList<>(); + Set uniqueSet = new HashSet<>(); for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) { filterChains.add( - parseFilterChain(fc, rdsResources, tlsContextManager, filterRegistry, parseHttpFilter)); + parseFilterChain(fc, rdsResources, tlsContextManager, filterRegistry, uniqueSet, + parseHttpFilter)); } FilterChain defaultFilterChain = null; if (proto.hasDefaultFilterChain()) { defaultFilterChain = parseFilterChain( proto.getDefaultFilterChain(), rdsResources, tlsContextManager, filterRegistry, - parseHttpFilter); + null, parseHttpFilter); } return new EnvoyServerProtoData.Listener( @@ -304,7 +307,8 @@ static EnvoyServerProtoData.Listener parseServerSideListener( @VisibleForTesting static FilterChain parseFilterChain( io.envoyproxy.envoy.config.listener.v3.FilterChain proto, Set rdsResources, - TlsContextManager tlsContextManager, FilterRegistry filterRegistry, boolean parseHttpFilters) + TlsContextManager tlsContextManager, FilterRegistry filterRegistry, + Set uniqueSet, boolean parseHttpFilters) throws ResourceInvalidException { io.grpc.xds.HttpConnectionManager httpConnectionManager = null; HashSet uniqueNames = new HashSet<>(); @@ -361,15 +365,144 @@ static FilterChain parseFilterChain( if (name.isEmpty()) { name = UUID.randomUUID().toString(); } + FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch()); + checkForUniqueness(uniqueSet, filterChainMatch); return new FilterChain( name, - parseFilterChainMatch(proto.getFilterChainMatch()), + filterChainMatch, httpConnectionManager, downstreamTlsContext, tlsContextManager ); } + private static void checkForUniqueness(Set uniqueSet, + FilterChainMatch filterChainMatch) throws ResourceInvalidException { + if (uniqueSet != null) { + List crossProduct = getCrossProduct(filterChainMatch); + for (FilterChainMatch cur : crossProduct) { + if (!uniqueSet.add(cur)) { + throw new ResourceInvalidException("Found duplicate matcher: " + cur); + } + } + } + } + + private static List getCrossProduct(FilterChainMatch filterChainMatch) { + // repeating fields to process: + // prefixRanges, applicationProtocols, sourcePrefixRanges, sourcePorts, serverNames + List expandedList = expandOnPrefixRange(filterChainMatch); + expandedList = expandOnApplicationProtocols(expandedList); + expandedList = expandOnSourcePrefixRange(expandedList); + expandedList = expandOnSourcePorts(expandedList); + return expandOnServerNames(expandedList); + } + + private static List expandOnPrefixRange(FilterChainMatch filterChainMatch) { + ArrayList expandedList = new ArrayList<>(); + if (filterChainMatch.getPrefixRanges().isEmpty()) { + expandedList.add(filterChainMatch); + } else { + for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.getPrefixRanges()) { + expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), + Arrays.asList(cidrRange), + Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), + Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), + filterChainMatch.getConnectionSourceType(), + Collections.unmodifiableList(filterChainMatch.getSourcePorts()), + Collections.unmodifiableList(filterChainMatch.getServerNames()), + filterChainMatch.getTransportProtocol())); + } + } + return expandedList; + } + + private static List expandOnApplicationProtocols( + Collection set) { + ArrayList expandedList = new ArrayList<>(); + for (FilterChainMatch filterChainMatch : set) { + if (filterChainMatch.getApplicationProtocols().isEmpty()) { + expandedList.add(filterChainMatch); + } else { + for (String applicationProtocol : filterChainMatch.getApplicationProtocols()) { + expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), + Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), + Arrays.asList(applicationProtocol), + Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), + filterChainMatch.getConnectionSourceType(), + Collections.unmodifiableList(filterChainMatch.getSourcePorts()), + Collections.unmodifiableList(filterChainMatch.getServerNames()), + filterChainMatch.getTransportProtocol())); + } + } + } + return expandedList; + } + + private static List expandOnSourcePrefixRange( + Collection set) { + ArrayList expandedList = new ArrayList<>(); + for (FilterChainMatch filterChainMatch : set) { + if (filterChainMatch.getSourcePrefixRanges().isEmpty()) { + expandedList.add(filterChainMatch); + } else { + for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.getSourcePrefixRanges()) { + expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), + Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), + Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), + Arrays.asList(cidrRange), + filterChainMatch.getConnectionSourceType(), + Collections.unmodifiableList(filterChainMatch.getSourcePorts()), + Collections.unmodifiableList(filterChainMatch.getServerNames()), + filterChainMatch.getTransportProtocol())); + } + } + } + return expandedList; + } + + private static List expandOnSourcePorts(Collection set) { + ArrayList expandedList = new ArrayList<>(); + for (FilterChainMatch filterChainMatch : set) { + if (filterChainMatch.getSourcePorts().isEmpty()) { + expandedList.add(filterChainMatch); + } else { + for (Integer sourcePort : filterChainMatch.getSourcePorts()) { + expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), + Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), + Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), + Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), + filterChainMatch.getConnectionSourceType(), + Arrays.asList(sourcePort), + Collections.unmodifiableList(filterChainMatch.getServerNames()), + filterChainMatch.getTransportProtocol())); + } + } + } + return expandedList; + } + + private static List expandOnServerNames(Collection set) { + ArrayList expandedList = new ArrayList<>(); + for (FilterChainMatch filterChainMatch : set) { + if (filterChainMatch.getServerNames().isEmpty()) { + expandedList.add(filterChainMatch); + } else { + for (String serverName : filterChainMatch.getServerNames()) { + expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), + Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), + Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), + Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), + filterChainMatch.getConnectionSourceType(), + Collections.unmodifiableList(filterChainMatch.getSourcePorts()), + Arrays.asList(serverName), + filterChainMatch.getTransportProtocol())); + } + } + } + return expandedList; + } + private static FilterChainMatch parseFilterChainMatch( io.envoyproxy.envoy.config.listener.v3.FilterChainMatch proto) throws ResourceInvalidException { diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java index 9ddce563875..b9e46127277 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java @@ -34,6 +34,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig.HashFunction; import io.envoyproxy.envoy.config.core.v3.Address; import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; +import io.envoyproxy.envoy.config.core.v3.CidrRange; import io.envoyproxy.envoy.config.core.v3.ConfigSource; import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions; import io.envoyproxy.envoy.config.core.v3.Locality; @@ -1038,6 +1039,153 @@ public void parseServerSideListener_useOriginalDst() throws ResourceInvalidExcep listener, new HashSet(), null, filterRegistry, true /* does not matter */); } + @Test + public void parseServerSideListener_nonUniqueFilterChainMatch() throws ResourceInvalidException { + Filter filter1 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-1").setIsOptional(true).build()); + FilterChainMatch filterChainMatch1 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(80, 8080)) + .addAllPrefixRanges(Arrays.asList(CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build(), + CidrRange.newBuilder().setAddressPrefix("10.0.0.0").setPrefixLen(UInt32Value.of(8)) + .build())) + .build(); + FilterChain filterChain1 = + FilterChain.newBuilder() + .setName("filter-chain-1") + .setFilterChainMatch(filterChainMatch1) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter1) + .build(); + Filter filter2 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-2").setIsOptional(true).build()); + FilterChainMatch filterChainMatch2 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(443, 8080)) + .addAllPrefixRanges(Arrays.asList( + CidrRange.newBuilder().setAddressPrefix("2001:DB8::8:800:200C:417A") + .setPrefixLen(UInt32Value.of(60)).build(), + CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build())) + .build(); + FilterChain filterChain2 = + FilterChain.newBuilder() + .setName("filter-chain-2") + .setFilterChainMatch(filterChainMatch2) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter2) + .build(); + Listener listener = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("Found duplicate matcher:"); + ClientXdsClient.parseServerSideListener( + listener, new HashSet(), null, filterRegistry, true /* does not matter */); + } + + @Test + public void parseServerSideListener_nonUniqueFilterChainMatch_sameFilter() + throws ResourceInvalidException { + Filter filter1 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-1").setIsOptional(true).build()); + FilterChainMatch filterChainMatch1 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(80, 8080)) + .addAllPrefixRanges(Arrays.asList( + CidrRange.newBuilder().setAddressPrefix("10.0.0.0").setPrefixLen(UInt32Value.of(8)) + .build())) + .build(); + FilterChain filterChain1 = + FilterChain.newBuilder() + .setName("filter-chain-1") + .setFilterChainMatch(filterChainMatch1) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter1) + .build(); + Filter filter2 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-2").setIsOptional(true).build()); + FilterChainMatch filterChainMatch2 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(443, 8080)) + .addAllPrefixRanges(Arrays.asList( + CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build(), + CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build())) + .build(); + FilterChain filterChain2 = + FilterChain.newBuilder() + .setName("filter-chain-2") + .setFilterChainMatch(filterChainMatch2) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter2) + .build(); + Listener listener = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("Found duplicate matcher:"); + ClientXdsClient.parseServerSideListener( + listener, new HashSet(), null, filterRegistry, true /* does not matter */); + } + + @Test + public void parseServerSideListener_uniqueFilterChainMatch() throws ResourceInvalidException { + Filter filter1 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-1").setIsOptional(true).build()); + FilterChainMatch filterChainMatch1 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(80, 8080)) + .addAllPrefixRanges(Arrays.asList(CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build(), + CidrRange.newBuilder().setAddressPrefix("10.0.0.0").setPrefixLen(UInt32Value.of(8)) + .build())) + .setSourceType(FilterChainMatch.ConnectionSourceType.EXTERNAL) + .build(); + FilterChain filterChain1 = + FilterChain.newBuilder() + .setName("filter-chain-1") + .setFilterChainMatch(filterChainMatch1) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter1) + .build(); + Filter filter2 = buildHttpConnectionManagerFilter( + HttpFilter.newBuilder().setName("http-filter-2").setIsOptional(true).build()); + FilterChainMatch filterChainMatch2 = + FilterChainMatch.newBuilder() + .addAllSourcePorts(Arrays.asList(443, 8080)) + .addAllPrefixRanges(Arrays.asList( + CidrRange.newBuilder().setAddressPrefix("2001:DB8::8:800:200C:417A") + .setPrefixLen(UInt32Value.of(60)).build(), + CidrRange.newBuilder().setAddressPrefix("192.168.0.0") + .setPrefixLen(UInt32Value.of(16)).build())) + .setSourceType(FilterChainMatch.ConnectionSourceType.ANY) + .build(); + FilterChain filterChain2 = + FilterChain.newBuilder() + .setName("filter-chain-2") + .setFilterChainMatch(filterChainMatch2) + .setTransportSocket(TransportSocket.getDefaultInstance()) + .addFilters(filter2) + .build(); + Listener listener = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) + .build(); + ClientXdsClient.parseServerSideListener( + listener, new HashSet(), null, filterRegistry, true /* does not matter */); + } + @Test public void parseFilterChain_noHcm() throws ResourceInvalidException { FilterChain filterChain = @@ -1050,7 +1198,7 @@ public void parseFilterChain_noHcm() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo missing required HttpConnectionManager filter"); ClientXdsClient.parseFilterChain( - filterChain, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain, new HashSet(), null, filterRegistry, null, true /* does not matter */); } @Test @@ -1068,7 +1216,7 @@ public void parseFilterChain_duplicateFilter() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo with duplicated filter: envoy.http_connection_manager"); ClientXdsClient.parseFilterChain( - filterChain, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain, new HashSet(), null, filterRegistry, null, true /* does not matter */); } @Test @@ -1086,7 +1234,7 @@ public void parseFilterChain_filterMissingTypedConfig() throws ResourceInvalidEx "FilterChain filter-chain-foo contains filter envoy.http_connection_manager " + "without typed_config"); ClientXdsClient.parseFilterChain( - filterChain, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain, new HashSet(), null, filterRegistry, null, true /* does not matter */); } @Test @@ -1108,7 +1256,7 @@ public void parseFilterChain_unsupportedFilter() throws ResourceInvalidException "FilterChain filter-chain-foo contains filter unsupported with unsupported " + "typed_config type unsupported-type-url"); ClientXdsClient.parseFilterChain( - filterChain, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain, new HashSet(), null, filterRegistry, null, true /* does not matter */); } @Test @@ -1133,9 +1281,11 @@ public void parseFilterChain_noName_generatedUuid() throws ResourceInvalidExcept .build(); EnvoyServerProtoData.FilterChain parsedFilterChain1 = ClientXdsClient.parseFilterChain( - filterChain1, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain1, new HashSet(), null, filterRegistry, null, + true /* does not matter */); EnvoyServerProtoData.FilterChain parsedFilterChain2 = ClientXdsClient.parseFilterChain( - filterChain2, new HashSet(), null, filterRegistry, true /* does not matter */); + filterChain2, new HashSet(), null, filterRegistry, null, + true /* does not matter */); assertThat(parsedFilterChain1.getName()).isNotEqualTo(parsedFilterChain2.getName()); }