• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19702

18 Feb 2025 06:47PM UTC coverage: 88.587% (-0.005%) from 88.592%
#19702

push

github

web-flow
xds: Change how xDS filters are created by introducing Filter.Provider (#11883)

This is the first step towards supporting filter state retention in
Java. The mechanism will be similar to the one described in [A83]
(https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache)
for C-core, and will serve the same purpose. However, the
implementation details are very different due to the different nature
of xDS HTTP filter support in C-core and Java.

In Java, xDS HTTP filters are backed by classes implementing
`io.grpc.xds.Filter`, from here just called "Filters". To support
Filter state retention (next PR), Java's xDS implementation must be
able to create unique Filter instances per:
- Per HCM
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager`
- Per filter name as specified in
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter.name`

This PR **does not** implements Filter state retention, but lays the
groundwork for it by changing how filters are registered and
instantiated. To achieve this, all existing Filter classes had to be
updated to the new instantiation mechanism described below.

Prior to these this PR, Filters had no livecycle. FilterRegistry
provided singleton instances for a given typeUrl. This PR introduces
a new interface `Filter.Provider`, which instantiates Filter classes.
All functionality that doesn't need an instance of a Filter is moved
to the Filter.Provider. This includes parsing filter config proto
into FilterConfig and determining the filter kind
(client-side, server-side, or both).

This PR is limited to refactoring, and there's no changes to the
existing behavior. Note that all Filter Providers still return
singleton Filter instances. However, with this PR, it is now possible
to create Providers that return a new Filter instance each time
`newInstance` is called.

34252 of 38665 relevant lines covered (88.59%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.89
/../xds/src/main/java/io/grpc/xds/XdsListenerResource.java
1
/*
2
 * Copyright 2022 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.checkNotNull;
20
import static io.grpc.xds.XdsClusterResource.validateCommonTlsContext;
21
import static io.grpc.xds.XdsRouteConfigureResource.extractVirtualHosts;
22
import static io.grpc.xds.client.XdsClient.ResourceUpdate;
23

24
import com.github.udpa.udpa.type.v1.TypedStruct;
25
import com.google.auto.value.AutoValue;
26
import com.google.common.annotations.VisibleForTesting;
27
import com.google.common.collect.ImmutableList;
28
import com.google.protobuf.Any;
29
import com.google.protobuf.InvalidProtocolBufferException;
30
import com.google.protobuf.Message;
31
import com.google.protobuf.util.Durations;
32
import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions;
33
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
34
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
35
import io.envoyproxy.envoy.config.listener.v3.Listener;
36
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
37
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds;
38
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext;
39
import io.grpc.xds.EnvoyServerProtoData.CidrRange;
40
import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType;
41
import io.grpc.xds.EnvoyServerProtoData.FilterChain;
42
import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch;
43
import io.grpc.xds.Filter.FilterConfig;
44
import io.grpc.xds.XdsListenerResource.LdsUpdate;
45
import io.grpc.xds.client.XdsResourceType;
46
import java.net.UnknownHostException;
47
import java.util.ArrayList;
48
import java.util.Collection;
49
import java.util.HashSet;
50
import java.util.List;
51
import java.util.Set;
52
import javax.annotation.Nullable;
53

54
class XdsListenerResource extends XdsResourceType<LdsUpdate> {
1✔
55
  static final String ADS_TYPE_URL_LDS =
56
      "type.googleapis.com/envoy.config.listener.v3.Listener";
57
  static final String TYPE_URL_HTTP_CONNECTION_MANAGER =
58
      "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3"
59
          + ".HttpConnectionManager";
60
  private static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
61
  private static final XdsListenerResource instance = new XdsListenerResource();
1✔
62
  private static final FilterRegistry filterRegistry = FilterRegistry.getDefaultRegistry();
1✔
63

64
  static XdsListenerResource getInstance() {
65
    return instance;
1✔
66
  }
67

68
  @Override
69
  @Nullable
70
  protected String extractResourceName(Message unpackedResource) {
71
    if (!(unpackedResource instanceof Listener)) {
1✔
72
      return null;
×
73
    }
74
    return ((Listener) unpackedResource).getName();
1✔
75
  }
76

77
  @Override
78
  public String typeName() {
79
    return "LDS";
1✔
80
  }
81

82
  @Override
83
  protected Class<Listener> unpackedClassName() {
84
    return Listener.class;
1✔
85
  }
86

87
  @Override
88
  public String typeUrl() {
89
    return ADS_TYPE_URL_LDS;
1✔
90
  }
91

92
  @Override
93
  public boolean shouldRetrieveResourceKeysForArgs() {
94
    return false;
1✔
95
  }
96

97
  @Override
98
  protected boolean isFullStateOfTheWorld() {
99
    return true;
1✔
100
  }
101

102
  @Override
103
  protected LdsUpdate doParse(Args args, Message unpackedMessage)
104
      throws ResourceInvalidException {
105
    if (!(unpackedMessage instanceof Listener)) {
1✔
106
      throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
×
107
    }
108
    Listener listener = (Listener) unpackedMessage;
1✔
109

110
    if (listener.hasApiListener()) {
1✔
111
      return processClientSideListener(listener, args);
1✔
112
    } else {
113
      return processServerSideListener(listener, args);
1✔
114
    }
115
  }
116

117
  private LdsUpdate processClientSideListener(Listener listener, XdsResourceType.Args args)
118
      throws ResourceInvalidException {
119
    // Unpack HttpConnectionManager from the Listener.
120
    HttpConnectionManager hcm;
121
    try {
122
      hcm = unpackCompatibleType(
1✔
123
          listener.getApiListener().getApiListener(), HttpConnectionManager.class,
1✔
124
          TYPE_URL_HTTP_CONNECTION_MANAGER, null);
125
    } catch (InvalidProtocolBufferException e) {
1✔
126
      throw new ResourceInvalidException(
1✔
127
          "Could not parse HttpConnectionManager config from ApiListener", e);
128
    }
1✔
129
    return LdsUpdate.forApiListener(
1✔
130
        parseHttpConnectionManager(hcm, filterRegistry, true /* isForClient */, args));
1✔
131
  }
132

133
  private LdsUpdate processServerSideListener(Listener proto, XdsResourceType.Args args)
134
      throws ResourceInvalidException {
135
    Set<String> certProviderInstances = null;
1✔
136
    if (args.getBootstrapInfo() != null && args.getBootstrapInfo().certProviders() != null) {
1✔
137
      certProviderInstances = args.getBootstrapInfo().certProviders().keySet();
1✔
138
    }
139
    return LdsUpdate.forTcpListener(parseServerSideListener(proto,
1✔
140
        (TlsContextManager) args.getSecurityConfig(),
1✔
141
        filterRegistry, certProviderInstances, args));
142
  }
143

144
  @VisibleForTesting
145
  static EnvoyServerProtoData.Listener parseServerSideListener(
146
      Listener proto, TlsContextManager tlsContextManager,
147
      FilterRegistry filterRegistry, Set<String> certProviderInstances, XdsResourceType.Args args)
148
      throws ResourceInvalidException {
149
    TrafficDirection trafficDirection = proto.getTrafficDirection();
1✔
150
    if (!trafficDirection.equals(TrafficDirection.INBOUND)
1✔
151
        && !trafficDirection.equals(TrafficDirection.UNSPECIFIED)) {
1✔
152
      throw new ResourceInvalidException(
1✔
153
          "Listener " + proto.getName() + " with invalid traffic direction: " + trafficDirection);
1✔
154
    }
155
    if (!proto.getListenerFiltersList().isEmpty()) {
1✔
156
      throw new ResourceInvalidException(
1✔
157
          "Listener " + proto.getName() + " cannot have listener_filters");
1✔
158
    }
159
    if (proto.hasUseOriginalDst()) {
1✔
160
      throw new ResourceInvalidException(
1✔
161
          "Listener " + proto.getName() + " cannot have use_original_dst set to true");
1✔
162
    }
163

164
    String address = null;
1✔
165
    if (proto.getAddress().hasSocketAddress()) {
1✔
166
      SocketAddress socketAddress = proto.getAddress().getSocketAddress();
1✔
167
      address = socketAddress.getAddress();
1✔
168
      switch (socketAddress.getPortSpecifierCase()) {
1✔
169
        case NAMED_PORT:
170
          address = address + ":" + socketAddress.getNamedPort();
×
171
          break;
×
172
        case PORT_VALUE:
173
          address = address + ":" + socketAddress.getPortValue();
1✔
174
          break;
1✔
175
        default:
176
          // noop
177
      }
178
    }
179

180
    ImmutableList.Builder<FilterChain> filterChains = ImmutableList.builder();
1✔
181
    Set<FilterChainMatch> filterChainMatchSet = new HashSet<>();
1✔
182
    int i = 0;
1✔
183
    for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) {
1✔
184
      // May be empty. If it's not empty, required to be unique.
185
      String filterChainName = fc.getName();
1✔
186
      if (filterChainName.isEmpty()) {
1✔
187
        // Generate a name, so we can identify it in the logs.
188
        filterChainName = "chain_" + i;
1✔
189
      }
190
      filterChains.add(
1✔
191
          parseFilterChain(fc, filterChainName, tlsContextManager, filterRegistry,
1✔
192
              filterChainMatchSet, certProviderInstances, args));
193
      i++;
1✔
194
    }
1✔
195

196
    FilterChain defaultFilterChain = null;
1✔
197
    if (proto.hasDefaultFilterChain()) {
1✔
198
      String defaultFilterChainName = proto.getDefaultFilterChain().getName();
1✔
199
      if (defaultFilterChainName.isEmpty()) {
1✔
200
        defaultFilterChainName = "chain_default";
1✔
201
      }
202
      defaultFilterChain = parseFilterChain(
1✔
203
          proto.getDefaultFilterChain(), defaultFilterChainName, tlsContextManager, filterRegistry,
1✔
204
          null, certProviderInstances, args);
205
    }
206

207
    return EnvoyServerProtoData.Listener.create(
1✔
208
        proto.getName(), address, filterChains.build(), defaultFilterChain);
1✔
209
  }
210

211
  @VisibleForTesting
212
  static FilterChain parseFilterChain(
213
      io.envoyproxy.envoy.config.listener.v3.FilterChain proto,
214
      String filterChainName,
215
      TlsContextManager tlsContextManager,
216
      FilterRegistry filterRegistry,
217
      // null disables FilterChainMatch uniqueness check, used for defaultFilterChain
218
      @Nullable Set<FilterChainMatch> filterChainMatchSet,
219
      Set<String> certProviderInstances,
220
      XdsResourceType.Args args)
221
      throws ResourceInvalidException {
222
    // FilterChain contains L4 filters, so we ensure it contains only HCM.
223
    if (proto.getFiltersCount() != 1) {
1✔
224
      throw new ResourceInvalidException("FilterChain " + filterChainName
1✔
225
          + " should contain exact one HttpConnectionManager filter");
226
    }
227
    io.envoyproxy.envoy.config.listener.v3.Filter l4Filter = proto.getFiltersList().get(0);
1✔
228
    if (!l4Filter.hasTypedConfig()) {
1✔
229
      throw new ResourceInvalidException(
1✔
230
          "FilterChain " + filterChainName + " contains filter " + l4Filter.getName()
1✔
231
              + " without typed_config");
232
    }
233
    Any any = l4Filter.getTypedConfig();
1✔
234
    if (!any.getTypeUrl().equals(TYPE_URL_HTTP_CONNECTION_MANAGER)) {
1✔
235
      throw new ResourceInvalidException(
1✔
236
          "FilterChain " + filterChainName + " contains filter " + l4Filter.getName()
1✔
237
              + " with unsupported typed_config type " + any.getTypeUrl());
1✔
238
    }
239

240
    // Parse HCM.
241
    HttpConnectionManager hcmProto;
242
    try {
243
      hcmProto = any.unpack(HttpConnectionManager.class);
1✔
244
    } catch (InvalidProtocolBufferException e) {
×
245
      throw new ResourceInvalidException("FilterChain " + filterChainName + " with filter "
×
246
          + l4Filter.getName() + " failed to unpack message", e);
×
247
    }
1✔
248
    io.grpc.xds.HttpConnectionManager httpConnectionManager = parseHttpConnectionManager(
1✔
249
        hcmProto, filterRegistry, false /* isForClient */, args);
250

251
    // Parse Transport Socket.
252
    EnvoyServerProtoData.DownstreamTlsContext downstreamTlsContext = null;
1✔
253
    if (proto.hasTransportSocket()) {
1✔
254
      if (!TRANSPORT_SOCKET_NAME_TLS.equals(proto.getTransportSocket().getName())) {
1✔
255
        throw new ResourceInvalidException("transport-socket with name "
1✔
256
            + proto.getTransportSocket().getName() + " not supported.");
1✔
257
      }
258
      DownstreamTlsContext downstreamTlsContextProto;
259
      try {
260
        downstreamTlsContextProto =
1✔
261
            proto.getTransportSocket().getTypedConfig().unpack(DownstreamTlsContext.class);
1✔
262
      } catch (InvalidProtocolBufferException e) {
×
263
        throw new ResourceInvalidException("FilterChain " + filterChainName
×
264
            + " failed to unpack message", e);
265
      }
1✔
266
      downstreamTlsContext =
1✔
267
          EnvoyServerProtoData.DownstreamTlsContext.fromEnvoyProtoDownstreamTlsContext(
1✔
268
              validateDownstreamTlsContext(downstreamTlsContextProto, certProviderInstances));
1✔
269
    }
270

271
    // Parse FilterChainMatch.
272
    FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch());
1✔
273
    // null used to skip this check for defaultFilterChain.
274
    if (filterChainMatchSet != null) {
1✔
275
      validateFilterChainMatchForUniqueness(filterChainMatchSet, filterChainMatch);
1✔
276
    }
277

278
    return FilterChain.create(
1✔
279
        filterChainName,
280
        filterChainMatch,
281
        httpConnectionManager,
282
        downstreamTlsContext,
283
        tlsContextManager
284
    );
285
  }
286

287
  @VisibleForTesting
288
  static DownstreamTlsContext validateDownstreamTlsContext(
289
      DownstreamTlsContext downstreamTlsContext, Set<String> certProviderInstances)
290
      throws ResourceInvalidException {
291
    if (downstreamTlsContext.hasCommonTlsContext()) {
1✔
292
      validateCommonTlsContext(downstreamTlsContext.getCommonTlsContext(), certProviderInstances,
1✔
293
          true);
294
    } else {
295
      throw new ResourceInvalidException(
1✔
296
          "common-tls-context is required in downstream-tls-context");
297
    }
298
    if (downstreamTlsContext.hasRequireSni()) {
1✔
299
      throw new ResourceInvalidException(
1✔
300
          "downstream-tls-context with require-sni is not supported");
301
    }
302
    DownstreamTlsContext.OcspStaplePolicy ocspStaplePolicy = downstreamTlsContext
1✔
303
        .getOcspStaplePolicy();
1✔
304
    if (ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.UNRECOGNIZED
1✔
305
        && ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.LENIENT_STAPLING) {
306
      throw new ResourceInvalidException(
1✔
307
          "downstream-tls-context with ocsp_staple_policy value " + ocspStaplePolicy.name()
1✔
308
              + " is not supported");
309
    }
310
    return downstreamTlsContext;
1✔
311
  }
312

313
  private static void validateFilterChainMatchForUniqueness(
314
      Set<FilterChainMatch> filterChainMatchSet,
315
      FilterChainMatch filterChainMatch) throws ResourceInvalidException {
316
    // Flattens complex FilterChainMatch into a list of simple FilterChainMatch'es.
317
    List<FilterChainMatch> crossProduct = getCrossProduct(filterChainMatch);
1✔
318
    for (FilterChainMatch cur : crossProduct) {
1✔
319
      if (!filterChainMatchSet.add(cur)) {
1✔
320
        throw new ResourceInvalidException("FilterChainMatch must be unique. "
1✔
321
            + "Found duplicate: " + cur);
322
      }
323
    }
1✔
324
  }
1✔
325

326
  private static List<FilterChainMatch> getCrossProduct(FilterChainMatch filterChainMatch) {
327
    // repeating fields to process:
328
    // prefixRanges, applicationProtocols, sourcePrefixRanges, sourcePorts, serverNames
329
    List<FilterChainMatch> expandedList = expandOnPrefixRange(filterChainMatch);
1✔
330
    expandedList = expandOnApplicationProtocols(expandedList);
1✔
331
    expandedList = expandOnSourcePrefixRange(expandedList);
1✔
332
    expandedList = expandOnSourcePorts(expandedList);
1✔
333
    return expandOnServerNames(expandedList);
1✔
334
  }
335

336
  private static List<FilterChainMatch> expandOnPrefixRange(FilterChainMatch filterChainMatch) {
337
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
1✔
338
    if (filterChainMatch.prefixRanges().isEmpty()) {
1✔
339
      expandedList.add(filterChainMatch);
1✔
340
    } else {
341
      for (CidrRange cidrRange : filterChainMatch.prefixRanges()) {
1✔
342
        expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
1✔
343
            ImmutableList.of(cidrRange),
1✔
344
            filterChainMatch.applicationProtocols(),
1✔
345
            filterChainMatch.sourcePrefixRanges(),
1✔
346
            filterChainMatch.connectionSourceType(),
1✔
347
            filterChainMatch.sourcePorts(),
1✔
348
            filterChainMatch.serverNames(),
1✔
349
            filterChainMatch.transportProtocol()));
1✔
350
      }
1✔
351
    }
352
    return expandedList;
1✔
353
  }
354

355
  private static List<FilterChainMatch> expandOnApplicationProtocols(
356
      Collection<FilterChainMatch> set) {
357
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
1✔
358
    for (FilterChainMatch filterChainMatch : set) {
1✔
359
      if (filterChainMatch.applicationProtocols().isEmpty()) {
1✔
360
        expandedList.add(filterChainMatch);
1✔
361
      } else {
362
        for (String applicationProtocol : filterChainMatch.applicationProtocols()) {
1✔
363
          expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
1✔
364
              filterChainMatch.prefixRanges(),
1✔
365
              ImmutableList.of(applicationProtocol),
1✔
366
              filterChainMatch.sourcePrefixRanges(),
1✔
367
              filterChainMatch.connectionSourceType(),
1✔
368
              filterChainMatch.sourcePorts(),
1✔
369
              filterChainMatch.serverNames(),
1✔
370
              filterChainMatch.transportProtocol()));
1✔
371
        }
1✔
372
      }
373
    }
1✔
374
    return expandedList;
1✔
375
  }
376

377
  private static List<FilterChainMatch> expandOnSourcePrefixRange(
378
      Collection<FilterChainMatch> set) {
379
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
1✔
380
    for (FilterChainMatch filterChainMatch : set) {
1✔
381
      if (filterChainMatch.sourcePrefixRanges().isEmpty()) {
1✔
382
        expandedList.add(filterChainMatch);
1✔
383
      } else {
384
        for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.sourcePrefixRanges()) {
×
385
          expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
×
386
              filterChainMatch.prefixRanges(),
×
387
              filterChainMatch.applicationProtocols(),
×
388
              ImmutableList.of(cidrRange),
×
389
              filterChainMatch.connectionSourceType(),
×
390
              filterChainMatch.sourcePorts(),
×
391
              filterChainMatch.serverNames(),
×
392
              filterChainMatch.transportProtocol()));
×
393
        }
×
394
      }
395
    }
1✔
396
    return expandedList;
1✔
397
  }
398

399
  private static List<FilterChainMatch> expandOnSourcePorts(Collection<FilterChainMatch> set) {
400
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
1✔
401
    for (FilterChainMatch filterChainMatch : set) {
1✔
402
      if (filterChainMatch.sourcePorts().isEmpty()) {
1✔
403
        expandedList.add(filterChainMatch);
1✔
404
      } else {
405
        for (Integer sourcePort : filterChainMatch.sourcePorts()) {
1✔
406
          expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
1✔
407
              filterChainMatch.prefixRanges(),
1✔
408
              filterChainMatch.applicationProtocols(),
1✔
409
              filterChainMatch.sourcePrefixRanges(),
1✔
410
              filterChainMatch.connectionSourceType(),
1✔
411
              ImmutableList.of(sourcePort),
1✔
412
              filterChainMatch.serverNames(),
1✔
413
              filterChainMatch.transportProtocol()));
1✔
414
        }
1✔
415
      }
416
    }
1✔
417
    return expandedList;
1✔
418
  }
419

420
  private static List<FilterChainMatch> expandOnServerNames(Collection<FilterChainMatch> set) {
421
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
1✔
422
    for (FilterChainMatch filterChainMatch : set) {
1✔
423
      if (filterChainMatch.serverNames().isEmpty()) {
1✔
424
        expandedList.add(filterChainMatch);
1✔
425
      } else {
426
        for (String serverName : filterChainMatch.serverNames()) {
×
427
          expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(),
×
428
              filterChainMatch.prefixRanges(),
×
429
              filterChainMatch.applicationProtocols(),
×
430
              filterChainMatch.sourcePrefixRanges(),
×
431
              filterChainMatch.connectionSourceType(),
×
432
              filterChainMatch.sourcePorts(),
×
433
              ImmutableList.of(serverName),
×
434
              filterChainMatch.transportProtocol()));
×
435
        }
×
436
      }
437
    }
1✔
438
    return expandedList;
1✔
439
  }
440

441
  private static FilterChainMatch parseFilterChainMatch(
442
      io.envoyproxy.envoy.config.listener.v3.FilterChainMatch proto)
443
      throws ResourceInvalidException {
444
    ImmutableList.Builder<CidrRange> prefixRanges = ImmutableList.builder();
1✔
445
    ImmutableList.Builder<CidrRange> sourcePrefixRanges = ImmutableList.builder();
1✔
446
    try {
447
      for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getPrefixRangesList()) {
1✔
448
        prefixRanges.add(
1✔
449
            CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue()));
1✔
450
      }
1✔
451
      for (io.envoyproxy.envoy.config.core.v3.CidrRange range
452
          : proto.getSourcePrefixRangesList()) {
1✔
453
        sourcePrefixRanges.add(
×
454
            CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue()));
×
455
      }
×
456
    } catch (UnknownHostException e) {
×
457
      throw new ResourceInvalidException("Failed to create CidrRange", e);
×
458
    }
1✔
459
    ConnectionSourceType sourceType;
460
    switch (proto.getSourceType()) {
1✔
461
      case ANY:
462
        sourceType = ConnectionSourceType.ANY;
1✔
463
        break;
1✔
464
      case EXTERNAL:
465
        sourceType = ConnectionSourceType.EXTERNAL;
1✔
466
        break;
1✔
467
      case SAME_IP_OR_LOOPBACK:
468
        sourceType = ConnectionSourceType.SAME_IP_OR_LOOPBACK;
×
469
        break;
×
470
      default:
471
        throw new ResourceInvalidException("Unknown source-type: " + proto.getSourceType());
×
472
    }
473
    return FilterChainMatch.create(
1✔
474
        proto.getDestinationPort().getValue(),
1✔
475
        prefixRanges.build(),
1✔
476
        ImmutableList.copyOf(proto.getApplicationProtocolsList()),
1✔
477
        sourcePrefixRanges.build(),
1✔
478
        sourceType,
479
        ImmutableList.copyOf(proto.getSourcePortsList()),
1✔
480
        ImmutableList.copyOf(proto.getServerNamesList()),
1✔
481
        proto.getTransportProtocol());
1✔
482
  }
483

484
  @VisibleForTesting
485
  static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager(
486
      HttpConnectionManager proto, FilterRegistry filterRegistry,
487
      boolean isForClient, XdsResourceType.Args args) throws ResourceInvalidException {
488
    if (proto.getXffNumTrustedHops() != 0) {
1✔
489
      throw new ResourceInvalidException(
1✔
490
          "HttpConnectionManager with xff_num_trusted_hops unsupported");
491
    }
492
    if (!proto.getOriginalIpDetectionExtensionsList().isEmpty()) {
1✔
493
      throw new ResourceInvalidException("HttpConnectionManager with "
1✔
494
          + "original_ip_detection_extensions unsupported");
495
    }
496
    // Obtain max_stream_duration from Http Protocol Options.
497
    long maxStreamDuration = 0;
1✔
498
    if (proto.hasCommonHttpProtocolOptions()) {
1✔
499
      HttpProtocolOptions options = proto.getCommonHttpProtocolOptions();
1✔
500
      if (options.hasMaxStreamDuration()) {
1✔
501
        maxStreamDuration = Durations.toNanos(options.getMaxStreamDuration());
1✔
502
      }
503
    }
504

505
    // Parse http filters.
506
    if (proto.getHttpFiltersList().isEmpty()) {
1✔
507
      throw new ResourceInvalidException("Missing HttpFilter in HttpConnectionManager.");
1✔
508
    }
509
    List<Filter.NamedFilterConfig> filterConfigs = new ArrayList<>();
1✔
510
    Set<String> names = new HashSet<>();
1✔
511
    for (int i = 0; i < proto.getHttpFiltersCount(); i++) {
1✔
512
      io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
513
          httpFilter = proto.getHttpFiltersList().get(i);
1✔
514
      String filterName = httpFilter.getName();
1✔
515
      if (!names.add(filterName)) {
1✔
516
        throw new ResourceInvalidException(
1✔
517
            "HttpConnectionManager contains duplicate HttpFilter: " + filterName);
518
      }
519
      StructOrError<Filter.FilterConfig> filterConfig =
1✔
520
          parseHttpFilter(httpFilter, filterRegistry, isForClient);
1✔
521
      if ((i == proto.getHttpFiltersCount() - 1)
1✔
522
          && (filterConfig == null || !isTerminalFilter(filterConfig.getStruct()))) {
1✔
523
        throw new ResourceInvalidException("The last HttpFilter must be a terminal filter: "
1✔
524
            + filterName);
525
      }
526
      if (filterConfig == null) {
1✔
527
        continue;
1✔
528
      }
529
      if (filterConfig.getErrorDetail() != null) {
1✔
530
        throw new ResourceInvalidException(
×
531
            "HttpConnectionManager contains invalid HttpFilter: "
532
                + filterConfig.getErrorDetail());
×
533
      }
534
      if ((i < proto.getHttpFiltersCount() - 1) && isTerminalFilter(filterConfig.getStruct())) {
1✔
535
        throw new ResourceInvalidException("A terminal HttpFilter must be the last filter: "
1✔
536
            + filterName);
537
      }
538
      filterConfigs.add(new Filter.NamedFilterConfig(filterName, filterConfig.getStruct()));
1✔
539
    }
540

541
    // Parse inlined RouteConfiguration or RDS.
542
    if (proto.hasRouteConfig()) {
1✔
543
      List<VirtualHost> virtualHosts = extractVirtualHosts(
1✔
544
          proto.getRouteConfig(), filterRegistry, args);
1✔
545
      return io.grpc.xds.HttpConnectionManager.forVirtualHosts(
1✔
546
          maxStreamDuration, virtualHosts, filterConfigs);
547
    }
548
    if (proto.hasRds()) {
1✔
549
      Rds rds = proto.getRds();
1✔
550
      if (!rds.hasConfigSource()) {
1✔
551
        throw new ResourceInvalidException(
×
552
            "HttpConnectionManager contains invalid RDS: missing config_source");
553
      }
554
      if (!rds.getConfigSource().hasAds() && !rds.getConfigSource().hasSelf()) {
1✔
555
        throw new ResourceInvalidException(
1✔
556
            "HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource");
557
      }
558
      return io.grpc.xds.HttpConnectionManager.forRdsName(
1✔
559
          maxStreamDuration, rds.getRouteConfigName(), filterConfigs);
1✔
560
    }
561
    throw new ResourceInvalidException(
1✔
562
        "HttpConnectionManager neither has inlined route_config nor RDS");
563
  }
564

565
  // hard-coded: currently router config is the only terminal filter.
566
  private static boolean isTerminalFilter(Filter.FilterConfig filterConfig) {
567
    return RouterFilter.ROUTER_CONFIG.equals(filterConfig);
1✔
568
  }
569

570
  @VisibleForTesting
571
  @Nullable // Returns null if the filter is optional but not supported.
572
  static StructOrError<Filter.FilterConfig> parseHttpFilter(
573
      io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
574
          httpFilter, FilterRegistry filterRegistry, boolean isForClient) {
575
    String filterName = httpFilter.getName();
1✔
576
    boolean isOptional = httpFilter.getIsOptional();
1✔
577
    if (!httpFilter.hasTypedConfig()) {
1✔
578
      return isOptional ? null : StructOrError.fromError(
1✔
579
          "HttpFilter [" + filterName + "] is not optional and has no typed config");
580
    }
581
    Message rawConfig = httpFilter.getTypedConfig();
1✔
582
    String typeUrl = httpFilter.getTypedConfig().getTypeUrl();
1✔
583

584
    try {
585
      if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA)) {
1✔
586
        TypedStruct typedStruct = httpFilter.getTypedConfig().unpack(TypedStruct.class);
1✔
587
        typeUrl = typedStruct.getTypeUrl();
1✔
588
        rawConfig = typedStruct.getValue();
1✔
589
      } else if (typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
1✔
590
        com.github.xds.type.v3.TypedStruct newTypedStruct =
×
591
            httpFilter.getTypedConfig().unpack(com.github.xds.type.v3.TypedStruct.class);
×
592
        typeUrl = newTypedStruct.getTypeUrl();
×
593
        rawConfig = newTypedStruct.getValue();
×
594
      }
595
    } catch (InvalidProtocolBufferException e) {
×
596
      return StructOrError.fromError(
×
597
          "HttpFilter [" + filterName + "] contains invalid proto: " + e);
598
    }
1✔
599

600
    Filter.Provider provider = filterRegistry.get(typeUrl);
1✔
601
    if (provider == null
1✔
602
        || (isForClient && !provider.isClientFilter())
1✔
603
        || (!isForClient && !provider.isServerFilter())) {
1✔
604
      // Filter type not supported.
605
      return isOptional ? null : StructOrError.fromError(
1✔
606
          "HttpFilter [" + filterName + "](" + typeUrl + ") is required but unsupported for " + (
607
              isForClient ? "client" : "server"));
1✔
608
    }
609
    ConfigOrError<? extends FilterConfig> filterConfig = provider.parseFilterConfig(rawConfig);
1✔
610
    if (filterConfig.errorDetail != null) {
1✔
611
      return StructOrError.fromError(
×
612
          "Invalid filter config for HttpFilter [" + filterName + "]: " + filterConfig.errorDetail);
613
    }
614
    return StructOrError.fromStruct(filterConfig.config);
1✔
615
  }
616

617
  @AutoValue
618
  abstract static class LdsUpdate implements ResourceUpdate {
1✔
619
    // Http level api listener configuration.
620
    @Nullable
621
    abstract io.grpc.xds.HttpConnectionManager httpConnectionManager();
622

623
    // Tcp level listener configuration.
624
    @Nullable
625
    abstract EnvoyServerProtoData.Listener listener();
626

627
    static LdsUpdate forApiListener(io.grpc.xds.HttpConnectionManager httpConnectionManager) {
628
      checkNotNull(httpConnectionManager, "httpConnectionManager");
1✔
629
      return new io.grpc.xds.AutoValue_XdsListenerResource_LdsUpdate(httpConnectionManager, null);
1✔
630
    }
631

632
    static LdsUpdate forTcpListener(EnvoyServerProtoData.Listener listener) {
633
      checkNotNull(listener, "listener");
1✔
634
      return new io.grpc.xds.AutoValue_XdsListenerResource_LdsUpdate(null, listener);
1✔
635
    }
636
  }
637
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc