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

grpc / grpc-java / #19717

06 Mar 2025 06:32PM UTC coverage: 88.525% (+0.03%) from 88.494%
#19717

push

github

web-flow
xds: Support filter state retention

This PR adds support 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.

### Filter instance lifecycle
#### xDS gRPC clients
New filter instances are created per combination of:
1. `XdsNameResolver` instance,
2. Filter name+typeUrl as configured in 
   HttpConnectionManager (HCM) http_filters.

Existing client-side filter instances are shutdown:
- A single a filter instance is shutdown when an LDS update contains
  HCM that is missing filter configuration for name+typeUrl
  combination of this instance.
- All filter instances when watched LDS resource is missing from an
  LDS update.
- All filter instances name resolver shutdown.

#### xDS-enabled gRPC servers
New filter instances are created per combination of:
1. Server instance,
2. FilterChain name,
3. Filter name+typeUrl as configured in FilterChain's HCM.http_filters

Filter instances of Default Filter Chain is tracked separately per:
1. Server instance,
2. Filter name+typeUrl in default_filter_chain's HCM.http_filters.

Existing server-side filter instances are shutdown:
- A single a filter instance is shutdown when an LDS update contains
  FilterChain with HCM.http_filters that is missing configuration for
  filter name+typeUrl.
- All filter instances associated with the FilterChain when an LDS
  update no longer contains FilterChain's name.
- All filter instances when watched LDS resource is missing from an
  LDS update.
- All filter instances on server shutdown.

### Related
- Part 1: #11883

34577 of 39059 relevant lines covered (88.53%)

0.89 hits per line

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

85.08
/../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.common.net.InetAddresses;
29
import com.google.protobuf.Any;
30
import com.google.protobuf.InvalidProtocolBufferException;
31
import com.google.protobuf.Message;
32
import com.google.protobuf.util.Durations;
33
import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions;
34
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
35
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
36
import io.envoyproxy.envoy.config.listener.v3.Listener;
37
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
38
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds;
39
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext;
40
import io.grpc.xds.EnvoyServerProtoData.CidrRange;
41
import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType;
42
import io.grpc.xds.EnvoyServerProtoData.FilterChain;
43
import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch;
44
import io.grpc.xds.Filter.FilterConfig;
45
import io.grpc.xds.XdsListenerResource.LdsUpdate;
46
import io.grpc.xds.client.XdsResourceType;
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<String> filterChainNames = new HashSet<>();
1✔
182
    Set<FilterChainMatch> filterChainMatchSet = new HashSet<>();
1✔
183
    int i = 0;
1✔
184
    for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) {
1✔
185
      // May be empty. If it's not empty, required to be unique.
186
      String filterChainName = fc.getName();
1✔
187
      if (filterChainName.isEmpty()) {
1✔
188
        // Generate a name, so we can identify it in the logs.
189
        filterChainName = "chain_" + i;
1✔
190
      }
191
      if (!filterChainNames.add(filterChainName)) {
1✔
192
        throw new ResourceInvalidException("Filter chain names must be unique. "
1✔
193
            + "Found duplicate: " + filterChainName);
194
      }
195
      filterChains.add(
1✔
196
          parseFilterChain(fc, filterChainName, tlsContextManager, filterRegistry,
1✔
197
              filterChainMatchSet, certProviderInstances, args));
198
      i++;
1✔
199
    }
1✔
200

201
    FilterChain defaultFilterChain = null;
1✔
202
    if (proto.hasDefaultFilterChain()) {
1✔
203
      String defaultFilterChainName = proto.getDefaultFilterChain().getName();
1✔
204
      if (defaultFilterChainName.isEmpty()) {
1✔
205
        defaultFilterChainName = "chain_default";
1✔
206
      }
207
      defaultFilterChain = parseFilterChain(
1✔
208
          proto.getDefaultFilterChain(), defaultFilterChainName, tlsContextManager, filterRegistry,
1✔
209
          null, certProviderInstances, args);
210
    }
211

212
    return EnvoyServerProtoData.Listener.create(
1✔
213
        proto.getName(), address, filterChains.build(), defaultFilterChain);
1✔
214
  }
215

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

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

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

276
    // Parse FilterChainMatch.
277
    FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch());
1✔
278
    // null used to skip this check for defaultFilterChain.
279
    if (filterChainMatchSet != null) {
1✔
280
      validateFilterChainMatchForUniqueness(filterChainMatchSet, filterChainMatch);
1✔
281
    }
282

283
    return FilterChain.create(
1✔
284
        filterChainName,
285
        filterChainMatch,
286
        httpConnectionManager,
287
        downstreamTlsContext,
288
        tlsContextManager
289
    );
290
  }
291

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

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

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

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

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

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

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

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

446
  private static FilterChainMatch parseFilterChainMatch(
447
      io.envoyproxy.envoy.config.listener.v3.FilterChainMatch proto)
448
      throws ResourceInvalidException {
449
    ImmutableList.Builder<CidrRange> prefixRanges = ImmutableList.builder();
1✔
450
    ImmutableList.Builder<CidrRange> sourcePrefixRanges = ImmutableList.builder();
1✔
451
    try {
452
      for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getPrefixRangesList()) {
1✔
453
        prefixRanges.add(
1✔
454
            CidrRange.create(InetAddresses.forString(range.getAddressPrefix()),
1✔
455
                range.getPrefixLen().getValue()));
1✔
456
      }
1✔
457
      for (io.envoyproxy.envoy.config.core.v3.CidrRange range
458
          : proto.getSourcePrefixRangesList()) {
1✔
459
        sourcePrefixRanges.add(CidrRange.create(
×
460
            InetAddresses.forString(range.getAddressPrefix()), range.getPrefixLen().getValue()));
×
461
      }
×
462
    } catch (IllegalArgumentException ex) {
×
463
      throw new ResourceInvalidException("Failed to create CidrRange", ex);
×
464
    }
1✔
465

466
    ConnectionSourceType sourceType;
467
    switch (proto.getSourceType()) {
1✔
468
      case ANY:
469
        sourceType = ConnectionSourceType.ANY;
1✔
470
        break;
1✔
471
      case EXTERNAL:
472
        sourceType = ConnectionSourceType.EXTERNAL;
1✔
473
        break;
1✔
474
      case SAME_IP_OR_LOOPBACK:
475
        sourceType = ConnectionSourceType.SAME_IP_OR_LOOPBACK;
×
476
        break;
×
477
      default:
478
        throw new ResourceInvalidException("Unknown source-type: " + proto.getSourceType());
×
479
    }
480
    return FilterChainMatch.create(
1✔
481
        proto.getDestinationPort().getValue(),
1✔
482
        prefixRanges.build(),
1✔
483
        ImmutableList.copyOf(proto.getApplicationProtocolsList()),
1✔
484
        sourcePrefixRanges.build(),
1✔
485
        sourceType,
486
        ImmutableList.copyOf(proto.getSourcePortsList()),
1✔
487
        ImmutableList.copyOf(proto.getServerNamesList()),
1✔
488
        proto.getTransportProtocol());
1✔
489
  }
490

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

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

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

572
  // hard-coded: currently router config is the only terminal filter.
573
  private static boolean isTerminalFilter(Filter.FilterConfig filterConfig) {
574
    return RouterFilter.ROUTER_CONFIG.equals(filterConfig);
1✔
575
  }
576

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

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

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

624
  @AutoValue
625
  abstract static class LdsUpdate implements ResourceUpdate {
1✔
626
    // Http level api listener configuration.
627
    @Nullable
628
    abstract io.grpc.xds.HttpConnectionManager httpConnectionManager();
629

630
    // Tcp level listener configuration.
631
    @Nullable
632
    abstract EnvoyServerProtoData.Listener listener();
633

634
    static LdsUpdate forApiListener(io.grpc.xds.HttpConnectionManager httpConnectionManager) {
635
      checkNotNull(httpConnectionManager, "httpConnectionManager");
1✔
636
      return new io.grpc.xds.AutoValue_XdsListenerResource_LdsUpdate(httpConnectionManager, null);
1✔
637
    }
638

639
    static LdsUpdate forTcpListener(EnvoyServerProtoData.Listener listener) {
640
      checkNotNull(listener, "listener");
1✔
641
      return new io.grpc.xds.AutoValue_XdsListenerResource_LdsUpdate(null, listener);
1✔
642
    }
643
  }
644
}
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