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

grpc / grpc-java / #20254

29 Apr 2026 05:08PM UTC coverage: 88.827% (+0.01%) from 88.814%
#20254

push

github

web-flow
xds: Trust Manager fix for when SAN validation against SNI sent doesn't apply (#12775)

Fixes a bug in propagation of `autoSniSanValidationDoesNotApply` (from
PR #12422). It added an argument `autoSniSanValidationDoesNotApply` to
`SslContextProviderSupplier.updateSslContext` that sets it on the
`DynamicSslContextProvider` but because `UpstreamTlsContext` equals
wasn't implemented, it was getting replaced by a new instance and the
flag getting lost. This issue was identified when fixing an incorrect
merge caused error in `CertProviderClientSslContextProvider` that
recreated the trust manager without consideration to
`autoSniSanValidationDoesNotApply`. It ought to have caused failure in
the test
`XdsSecurityClientServerTest.tlsClientServer_autoSniValidation_noSniApplicable_usesMatcherFromCmnVdnCtx`
but it wasn't, because even though `autoSniSanValidationDoesNotApply`
was false due to not getting the propagated true value, SAN matcher
fallback was still happening because there was no server SNI sent.

With the new changes, in addition to fixing the equals method, by moving
the decision about autoSniSanValidationDoesNotApply to
TlsContextManagerImpl.findOrCreateClientSslContextProvider I have
eliminated the need to have a deferred setting of this decision via
DynamicSslContextProvider.setAutoSniSanValidationDoesNotApply called
from SslContextProviderSupplier.updateSslContext.

Summary of Changes:

   1. Enhanced `UpstreamTlsContext` (EnvoyServerProtoData.java):
* Modified Caching Behavior: Implemented full equals() and hashCode()
overrides for `UpstreamTlsContext`. Previously, it relied on the base
class which only compared the commonTlsContext, causing different SNI or
auto-validation settings to incorrectly share the same cache entry.
* Normalization: Updated constructors to normalize the sni field to an
empty string ("") if null. This prevents equality mismatches between
context objects created from different sources (e.g., test helpers vs.
Envoy proto... (continued)

36141 of 40687 relevant lines covered (88.83%)

0.89 hits per line

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

94.34
/../xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java
1
/*
2
 * Copyright 2020 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

21
import com.google.auto.value.AutoValue;
22
import com.google.common.annotations.VisibleForTesting;
23
import com.google.common.collect.ImmutableList;
24
import com.google.protobuf.util.Durations;
25
import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol;
26
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
27
import io.grpc.Internal;
28
import io.grpc.xds.client.EnvoyProtoData;
29
import io.grpc.xds.internal.security.SslContextProviderSupplier;
30
import java.net.InetAddress;
31
import java.util.Objects;
32
import javax.annotation.Nullable;
33

34
/**
35
 * Defines gRPC data types for Envoy protobuf messages used in xDS protocol on the server side,
36
 * similar to how {@link EnvoyProtoData} defines it for the client side.
37
 */
38
@Internal
39
public final class EnvoyServerProtoData {
40

41
  // Prevent instantiation.
42
  private EnvoyServerProtoData() {
43
  }
44

45
  public abstract static class BaseTlsContext {
46
    protected final CommonTlsContext commonTlsContext;
47

48
    protected BaseTlsContext(CommonTlsContext commonTlsContext) {
1✔
49
      this.commonTlsContext = checkNotNull(commonTlsContext, "commonTlsContext cannot be null.");
1✔
50
    }
1✔
51

52
    public CommonTlsContext getCommonTlsContext() {
53
      return commonTlsContext;
1✔
54
    }
55

56
    @Override
57
    public boolean equals(Object o) {
58
      if (this == o) {
1✔
59
        return true;
×
60
      }
61
      if (!(o instanceof BaseTlsContext)) {
1✔
62
        return false;
×
63
      }
64
      BaseTlsContext that = (BaseTlsContext) o;
1✔
65
      return Objects.equals(commonTlsContext, that.commonTlsContext);
1✔
66
    }
67

68
    @Override
69
    public int hashCode() {
70
      return Objects.hashCode(commonTlsContext);
1✔
71
    }
72
  }
73

74
  public static final class UpstreamTlsContext extends BaseTlsContext {
75

76
    private final String sni;
77
    private final boolean autoHostSni;
78
    private final boolean autoSniSanValidation;
79

80
    @VisibleForTesting
81
    public UpstreamTlsContext(CommonTlsContext commonTlsContext) {
82
      this(commonTlsContext, "", false, false);
1✔
83
    }
1✔
84

85
    @VisibleForTesting
86
    public UpstreamTlsContext(
87
        CommonTlsContext commonTlsContext, String sni, boolean autoHostSni,
88
        boolean autoSniSanValidation) {
89
      super(commonTlsContext);
1✔
90
      this.sni = sni == null ? "" : sni;
1✔
91
      this.autoHostSni = autoHostSni;
1✔
92
      this.autoSniSanValidation = autoSniSanValidation;
1✔
93
    }
1✔
94

95
    @VisibleForTesting
96
    public UpstreamTlsContext(
97
        io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
98
            upstreamTlsContext) {
99
      super(upstreamTlsContext.getCommonTlsContext());
1✔
100
      this.sni = upstreamTlsContext.getSni();
1✔
101
      this.autoHostSni = upstreamTlsContext.getAutoHostSni();
1✔
102
      this.autoSniSanValidation = upstreamTlsContext.getAutoSniSanValidation();
1✔
103
    }
1✔
104

105
    public static UpstreamTlsContext fromEnvoyProtoUpstreamTlsContext(
106
        io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
107
            upstreamTlsContext) {
108
      return new UpstreamTlsContext(upstreamTlsContext);
1✔
109
    }
110

111
    public String getSni() {
112
      return sni;
1✔
113
    }
114

115
    public boolean getAutoHostSni() {
116
      return autoHostSni;
1✔
117
    }
118

119
    public boolean getAutoSniSanValidation() {
120
      return autoSniSanValidation;
1✔
121
    }
122

123
    @Override
124
    public String toString() {
125
      return "UpstreamTlsContext{"
1✔
126
          + "commonTlsContext=" + commonTlsContext
127
          + "\nsni=" + sni
128
          + "\nauto_host_sni=" + autoHostSni
129
          + "\nauto_sni_san_validation=" + autoSniSanValidation
130
          + "}";
131
    }
132

133
    @Override
134
    public boolean equals(Object o) {
135
      if (this == o) {
1✔
136
        return true;
×
137
      }
138
      if (o == null || getClass() != o.getClass()) {
1✔
139
        return false;
1✔
140
      }
141
      UpstreamTlsContext that = (UpstreamTlsContext) o;
1✔
142
      return autoHostSni == that.autoHostSni
1✔
143
          && autoSniSanValidation == that.autoSniSanValidation
144
          && Objects.equals(commonTlsContext, that.commonTlsContext)
1✔
145
          && Objects.equals(sni, that.sni);
1✔
146
    }
147

148
    @Override
149
    public int hashCode() {
150
      return Objects.hash(commonTlsContext, sni, autoHostSni, autoSniSanValidation);
1✔
151
    }
152
  }
153

154
  public static final class DownstreamTlsContext extends BaseTlsContext {
155

156
    private final boolean requireClientCertificate;
157

158
    @VisibleForTesting
159
    public DownstreamTlsContext(
160
        CommonTlsContext commonTlsContext, boolean requireClientCertificate) {
161
      super(commonTlsContext);
1✔
162
      this.requireClientCertificate = requireClientCertificate;
1✔
163
    }
1✔
164

165
    public static DownstreamTlsContext fromEnvoyProtoDownstreamTlsContext(
166
        io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
167
            downstreamTlsContext) {
168
      return new DownstreamTlsContext(downstreamTlsContext.getCommonTlsContext(),
1✔
169
        downstreamTlsContext.hasRequireClientCertificate());
1✔
170
    }
171

172
    public boolean isRequireClientCertificate() {
173
      return requireClientCertificate;
1✔
174
    }
175

176
    @Override
177
    public String toString() {
178
      return "DownstreamTlsContext{"
×
179
          + "commonTlsContext="
180
          + commonTlsContext
181
          + ", requireClientCertificate="
182
          + requireClientCertificate
183
          + '}';
184
    }
185

186
    @Override
187
    public boolean equals(Object o) {
188
      if (this == o) {
1✔
189
        return true;
×
190
      }
191
      if (o == null || getClass() != o.getClass()) {
1✔
192
        return false;
×
193
      }
194
      if (!super.equals(o)) {
1✔
195
        return false;
1✔
196
      }
197
      DownstreamTlsContext that = (DownstreamTlsContext) o;
1✔
198
      return requireClientCertificate == that.requireClientCertificate;
1✔
199
    }
200

201
    @Override
202
    public int hashCode() {
203
      return Objects.hash(super.hashCode(), requireClientCertificate);
1✔
204
    }
205
  }
206

207
  @AutoValue
208
  abstract static class CidrRange {
1✔
209

210
    abstract InetAddress addressPrefix();
211

212
    abstract int prefixLen();
213

214
    static CidrRange create(InetAddress addressPrefix, int prefixLen) {
215
      return new AutoValue_EnvoyServerProtoData_CidrRange(
1✔
216
          addressPrefix, prefixLen);
217
    }
218
  }
219

220
  enum ConnectionSourceType {
1✔
221
    // Any connection source matches.
222
    ANY,
1✔
223

224
    // Match a connection originating from the same host.
225
    SAME_IP_OR_LOOPBACK,
1✔
226

227
    // Match a connection originating from a different host.
228
    EXTERNAL
1✔
229
  }
230

231
  /**
232
   * Corresponds to Envoy proto message
233
   * {@link io.envoyproxy.envoy.config.listener.v3.FilterChainMatch}.
234
   */
235
  @AutoValue
236
  abstract static class FilterChainMatch {
1✔
237

238
    abstract int destinationPort();
239

240
    abstract ImmutableList<CidrRange> prefixRanges();
241

242
    abstract ImmutableList<String> applicationProtocols();
243

244
    abstract ImmutableList<CidrRange> sourcePrefixRanges();
245

246
    abstract ConnectionSourceType connectionSourceType();
247

248
    abstract ImmutableList<Integer> sourcePorts();
249

250
    abstract ImmutableList<String> serverNames();
251

252
    abstract String transportProtocol();
253

254
    public static FilterChainMatch create(int destinationPort,
255
        ImmutableList<CidrRange> prefixRanges,
256
        ImmutableList<String> applicationProtocols, ImmutableList<CidrRange> sourcePrefixRanges,
257
        ConnectionSourceType connectionSourceType, ImmutableList<Integer> sourcePorts,
258
        ImmutableList<String> serverNames, String transportProtocol) {
259
      return new AutoValue_EnvoyServerProtoData_FilterChainMatch(
1✔
260
          destinationPort, prefixRanges, applicationProtocols, sourcePrefixRanges,
261
          connectionSourceType, sourcePorts, serverNames, transportProtocol);
262
    }
263
  }
264

265
  /**
266
   * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.FilterChain}.
267
   */
268
  @AutoValue
269
  abstract static class FilterChain {
1✔
270

271
    // Must be unique per server instance (except the default chain).
272
    abstract String name();
273

274
    // TODO(sanjaypujare): flatten structure by moving FilterChainMatch class members here.
275
    abstract FilterChainMatch filterChainMatch();
276

277
    abstract HttpConnectionManager httpConnectionManager();
278

279
    @Nullable
280
    abstract SslContextProviderSupplier sslContextProviderSupplier();
281

282
    static FilterChain create(
283
        String name,
284
        FilterChainMatch filterChainMatch,
285
        HttpConnectionManager httpConnectionManager,
286
        @Nullable DownstreamTlsContext downstreamTlsContext,
287
        TlsContextManager tlsContextManager) {
288
      SslContextProviderSupplier sslContextProviderSupplier =
289
          downstreamTlsContext == null
1✔
290
              ? null : new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager);
1✔
291
      return new AutoValue_EnvoyServerProtoData_FilterChain(
1✔
292
          name, filterChainMatch, httpConnectionManager, sslContextProviderSupplier);
293
    }
294
  }
295

296
  /**
297
   * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.Listener} and
298
   * related classes.
299
   */
300
  @AutoValue
301
  abstract static class Listener {
1✔
302

303
    abstract String name();
304

305
    @Nullable
306
    abstract String address();
307

308
    abstract ImmutableList<FilterChain> filterChains();
309

310
    @Nullable
311
    abstract FilterChain defaultFilterChain();
312

313
    @Nullable
314
    abstract Protocol protocol();
315

316
    static Listener create(
317
        String name,
318
        @Nullable String address,
319
        ImmutableList<FilterChain> filterChains,
320
        @Nullable FilterChain defaultFilterChain,
321
        @Nullable Protocol protocol) {
322
      return new AutoValue_EnvoyServerProtoData_Listener(name, address, filterChains,
1✔
323
          defaultFilterChain, protocol);
324
    }
325
  }
326

327
  /**
328
   * Corresponds to Envoy proto message {@link
329
   * io.envoyproxy.envoy.config.cluster.v3.OutlierDetection}. Only the fields supported by gRPC are
330
   * included.
331
   *
332
   * <p>Protobuf Duration fields are represented in their string format (e.g. "10s").
333
   */
334
  @AutoValue
335
  abstract static class OutlierDetection {
1✔
336

337
    @Nullable
338
    abstract Long intervalNanos();
339

340
    @Nullable
341
    abstract Long baseEjectionTimeNanos();
342

343
    @Nullable
344
    abstract Long maxEjectionTimeNanos();
345

346
    @Nullable
347
    abstract Integer maxEjectionPercent();
348

349
    @Nullable
350
    abstract SuccessRateEjection successRateEjection();
351

352
    @Nullable
353
    abstract FailurePercentageEjection failurePercentageEjection();
354

355
    static OutlierDetection create(
356
        @Nullable Long intervalNanos,
357
        @Nullable Long baseEjectionTimeNanos,
358
        @Nullable Long maxEjectionTimeNanos,
359
        @Nullable Integer maxEjectionPercentage,
360
        @Nullable SuccessRateEjection successRateEjection,
361
        @Nullable FailurePercentageEjection failurePercentageEjection) {
362
      return new AutoValue_EnvoyServerProtoData_OutlierDetection(intervalNanos,
1✔
363
          baseEjectionTimeNanos, maxEjectionTimeNanos, maxEjectionPercentage, successRateEjection,
364
          failurePercentageEjection);
365
    }
366

367
    static OutlierDetection fromEnvoyOutlierDetection(
368
        io.envoyproxy.envoy.config.cluster.v3.OutlierDetection envoyOutlierDetection) {
369

370
      Long intervalNanos = envoyOutlierDetection.hasInterval()
1✔
371
          ? Durations.toNanos(envoyOutlierDetection.getInterval()) : null;
1✔
372
      Long baseEjectionTimeNanos = envoyOutlierDetection.hasBaseEjectionTime()
1✔
373
          ? Durations.toNanos(envoyOutlierDetection.getBaseEjectionTime()) : null;
1✔
374
      Long maxEjectionTimeNanos = envoyOutlierDetection.hasMaxEjectionTime()
1✔
375
          ? Durations.toNanos(envoyOutlierDetection.getMaxEjectionTime()) : null;
1✔
376
      Integer maxEjectionPercentage = envoyOutlierDetection.hasMaxEjectionPercent()
1✔
377
          ? envoyOutlierDetection.getMaxEjectionPercent().getValue() : null;
1✔
378

379
      SuccessRateEjection successRateEjection;
380
      // If success rate enforcement has been turned completely off, don't configure this ejection.
381
      if (envoyOutlierDetection.hasEnforcingSuccessRate()
1✔
382
          && envoyOutlierDetection.getEnforcingSuccessRate().getValue() == 0) {
1✔
383
        successRateEjection = null;
1✔
384
      } else {
385
        Integer stdevFactor = envoyOutlierDetection.hasSuccessRateStdevFactor()
1✔
386
            ? envoyOutlierDetection.getSuccessRateStdevFactor().getValue() : null;
1✔
387
        Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingSuccessRate()
1✔
388
            ? envoyOutlierDetection.getEnforcingSuccessRate().getValue() : null;
1✔
389
        Integer minimumHosts = envoyOutlierDetection.hasSuccessRateMinimumHosts()
1✔
390
            ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null;
1✔
391
        Integer requestVolume = envoyOutlierDetection.hasSuccessRateRequestVolume()
1✔
392
            ? envoyOutlierDetection.getSuccessRateRequestVolume().getValue() : null;
1✔
393

394
        successRateEjection = SuccessRateEjection.create(stdevFactor, enforcementPercentage,
1✔
395
            minimumHosts, requestVolume);
396
      }
397

398
      FailurePercentageEjection failurePercentageEjection;
399
      if (envoyOutlierDetection.hasEnforcingFailurePercentage()
1✔
400
          && envoyOutlierDetection.getEnforcingFailurePercentage().getValue() == 0) {
1✔
401
        failurePercentageEjection = null;
1✔
402
      } else {
403
        Integer threshold = envoyOutlierDetection.hasFailurePercentageThreshold()
1✔
404
            ? envoyOutlierDetection.getFailurePercentageThreshold().getValue() : null;
1✔
405
        Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingFailurePercentage()
1✔
406
            ? envoyOutlierDetection.getEnforcingFailurePercentage().getValue() : null;
1✔
407
        Integer minimumHosts = envoyOutlierDetection.hasFailurePercentageMinimumHosts()
1✔
408
            ? envoyOutlierDetection.getFailurePercentageMinimumHosts().getValue() : null;
1✔
409
        Integer requestVolume = envoyOutlierDetection.hasFailurePercentageRequestVolume()
1✔
410
            ? envoyOutlierDetection.getFailurePercentageRequestVolume().getValue() : null;
1✔
411

412
        failurePercentageEjection = FailurePercentageEjection.create(threshold,
1✔
413
            enforcementPercentage, minimumHosts, requestVolume);
414
      }
415

416
      return create(intervalNanos, baseEjectionTimeNanos, maxEjectionTimeNanos,
1✔
417
          maxEjectionPercentage, successRateEjection, failurePercentageEjection);
418
    }
419
  }
420

421
  @AutoValue
422
  abstract static class SuccessRateEjection {
1✔
423

424
    @Nullable
425
    abstract Integer stdevFactor();
426

427
    @Nullable
428
    abstract Integer enforcementPercentage();
429

430
    @Nullable
431
    abstract Integer minimumHosts();
432

433
    @Nullable
434
    abstract Integer requestVolume();
435

436
    static SuccessRateEjection create(
437
        @Nullable Integer stdevFactor,
438
        @Nullable Integer enforcementPercentage,
439
        @Nullable Integer minimumHosts,
440
        @Nullable Integer requestVolume) {
441
      return new AutoValue_EnvoyServerProtoData_SuccessRateEjection(stdevFactor,
1✔
442
          enforcementPercentage, minimumHosts, requestVolume);
443
    }
444
  }
445

446
  @AutoValue
447
  abstract static class FailurePercentageEjection {
1✔
448

449
    @Nullable
450
    abstract Integer threshold();
451

452
    @Nullable
453
    abstract Integer enforcementPercentage();
454

455
    @Nullable
456
    abstract Integer minimumHosts();
457

458
    @Nullable
459
    abstract Integer requestVolume();
460

461
    static FailurePercentageEjection create(
462
        @Nullable Integer threshold,
463
        @Nullable Integer enforcementPercentage,
464
        @Nullable Integer minimumHosts,
465
        @Nullable Integer requestVolume) {
466
      return new AutoValue_EnvoyServerProtoData_FailurePercentageEjection(threshold,
1✔
467
          enforcementPercentage, minimumHosts, requestVolume);
468
    }
469
  }
470
}
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