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

grpc / grpc-java / #19762

03 Apr 2025 05:52AM UTC coverage: 88.598% (-0.005%) from 88.603%
#19762

push

github

web-flow
xds: listener type validation (#11933)

34732 of 39202 relevant lines covered (88.6%)

0.89 hits per line

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

85.8
/../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
    SocketAddress socketAddress = null;
1✔
166
    if (proto.getAddress().hasSocketAddress()) {
1✔
167
      socketAddress = proto.getAddress().getSocketAddress();
1✔
168
      address = socketAddress.getAddress();
1✔
169
      if (address.isEmpty()) {
1✔
170
        throw new ResourceInvalidException("Invalid address: Empty address is not allowed.");
1✔
171
      }
172
      switch (socketAddress.getPortSpecifierCase()) {
1✔
173
        case NAMED_PORT:
174
          throw new ResourceInvalidException("NAMED_PORT is not supported in gRPC.");
1✔
175
        case PORT_VALUE:
176
          address = address + ":" + socketAddress.getPortValue();
1✔
177
          break;
1✔
178
        default:
179
          // noop
180
      }
181
    }
182

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

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

215
    return EnvoyServerProtoData.Listener.create(proto.getName(), address, filterChains.build(),
1✔
216
        defaultFilterChain, socketAddress == null ? null : socketAddress.getProtocol());
1✔
217
  }
218

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

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

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

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

286
    return FilterChain.create(
1✔
287
        filterChainName,
288
        filterChainMatch,
289
        httpConnectionManager,
290
        downstreamTlsContext,
291
        tlsContextManager
292
    );
293
  }
294

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

633
    // Tcp level listener configuration.
634
    @Nullable
635
    abstract EnvoyServerProtoData.Listener listener();
636

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

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

© 2025 Coveralls, Inc