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

grpc / grpc-java / #20170

09 Feb 2026 10:46AM UTC coverage: 88.695% (+0.001%) from 88.694%
#20170

push

github

web-flow
fix(xds): Allow and normalize trailing dot (FQDN) in matchHostName (#12644)

## Summary

`matchHostName` in `RoutingUtils` and `XdsNameResolver` currently
rejects hostnames and patterns
with a trailing dot (`.`) via `checkArgument`. A trailing dot denotes a
**Fully Qualified Domain Name (FQDN)** as defined in
[RFC 1034 Section
3.1](https://www.rfc-editor.org/rfc/rfc1034#section-3.1), and is a
valid,
well-defined representation of an absolute domain name. Rejecting it is
inconsistent with the RFC.

This change removes the trailing-dot rejection and adds normalization to
strip the trailing dot
before matching, making `example.com.` and `example.com` match
equivalently.

## Background

Per [RFC 1034 Section
3.1](https://www.rfc-editor.org/rfc/rfc1034#section-3.1):

> "If the name ends with a dot, it is an absolute name ... For example,
`poneria.ISI.EDU.`"

A trailing dot simply indicates that the name is rooted at the DNS root
and is semantically
equivalent to the same name without the trailing dot. Treating it as
invalid prevents legitimate
FQDNs from being used as hostnames or virtual host domain patterns in
xDS routing configuration.

## Motivation

This was discovered when using gRPC Proxyless Service Mesh on a
Kubernetes cluster with Istio.
The issue surfaced after upgrading Istio from 1.26.8 to 1.28.3. The
Istio change
[istio/istio#56008](https://github.com/istio/istio/pull/56008) began
sending FQDN-style domain
names (with trailing dots) in xDS route configuration, which caused
grpc-java to throw an
`IllegalArgumentException` in `matchHostName`:

```text
java.lang.IllegalArgumentException: Invalid pattern/domain name
    at com.google.common.base.Preconditions.checkArgument(Preconditions.java:143)
```

The root cause is that grpc-java's `matchHostName` was not RFC-compliant
in rejecting trailing dots — the Istio upgrade merely made it visible.
The fix here is to bring grpc-java into compliance with RFC 1034,
independent of any specific Istio versi... (continued)

35391 of 39902 relevant lines covered (88.69%)

0.89 hits per line

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

94.59
/../xds/src/main/java/io/grpc/xds/RoutingUtils.java
1
/*
2
 * Copyright 2021 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.checkArgument;
20

21
import com.google.common.base.Joiner;
22
import io.grpc.Metadata;
23
import io.grpc.xds.VirtualHost.Route.RouteMatch;
24
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
25
import io.grpc.xds.internal.Matchers.FractionMatcher;
26
import io.grpc.xds.internal.Matchers.HeaderMatcher;
27
import java.util.List;
28
import java.util.Locale;
29
import javax.annotation.Nullable;
30

31
/**
32
 * Utilities for performing virtual host domain name matching and route matching.
33
 */
34
// TODO(chengyuanzhang): clean up implementations in XdsNameResolver.
35
final class RoutingUtils {
36
  // Prevent instantiation.
37
  private RoutingUtils() {
38
  }
39

40
  /**
41
   * Returns the {@link VirtualHost} with the best match domain for the given hostname.
42
   */
43
  @Nullable
44
  static VirtualHost findVirtualHostForHostName(List<VirtualHost> virtualHosts, String hostName) {
45
    // Domain search order:
46
    //  1. Exact domain names: ``www.foo.com``.
47
    //  2. Suffix domain wildcards: ``*.foo.com`` or ``*-bar.foo.com``.
48
    //  3. Prefix domain wildcards: ``foo.*`` or ``foo-*``.
49
    //  4. Special wildcard ``*`` matching any domain.
50
    //
51
    //  The longest wildcards match first.
52
    //  Assuming only a single virtual host in the entire route configuration can match
53
    //  on ``*`` and a domain must be unique across all virtual hosts.
54
    int matchingLen = -1; // longest length of wildcard pattern that matches host name
1✔
55
    boolean exactMatchFound = false;  // true if a virtual host with exactly matched domain found
1✔
56
    VirtualHost targetVirtualHost = null;  // target VirtualHost with longest matched domain
1✔
57
    for (VirtualHost vHost : virtualHosts) {
1✔
58
      for (String domain : vHost.domains()) {
1✔
59
        boolean selected = false;
1✔
60
        if (matchHostName(hostName, domain)) { // matching
1✔
61
          if (!domain.contains("*")) { // exact matching
1✔
62
            exactMatchFound = true;
1✔
63
            targetVirtualHost = vHost;
1✔
64
            break;
1✔
65
          } else if (domain.length() > matchingLen) { // longer matching pattern
1✔
66
            selected = true;
1✔
67
          } else if (domain.length() == matchingLen && domain.startsWith("*")) { // suffix matching
1✔
68
            selected = true;
×
69
          }
70
        }
71
        if (selected) {
1✔
72
          matchingLen = domain.length();
1✔
73
          targetVirtualHost = vHost;
1✔
74
        }
75
      }
1✔
76
      if (exactMatchFound) {
1✔
77
        break;
1✔
78
      }
79
    }
1✔
80
    return targetVirtualHost;
1✔
81
  }
82

83
  /**
84
   * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with
85
   * case-insensitive.
86
   *
87
   * <p>Wildcard pattern rules:
88
   * <ol>
89
   * <li>A single asterisk (*) matches any domain.</li>
90
   * <li>Asterisk (*) is only permitted in the left-most or the right-most part of the pattern,
91
   *     but not both.</li>
92
   * </ol>
93
   */
94
  private static boolean matchHostName(String hostName, String pattern) {
95
    checkArgument(hostName.length() != 0 && !hostName.startsWith("."),
1✔
96
        "Invalid host name");
97
    checkArgument(pattern.length() != 0 && !pattern.startsWith("."),
1✔
98
        "Invalid pattern/domain name");
99

100
    hostName = hostName.toLowerCase(Locale.US);
1✔
101
    pattern = pattern.toLowerCase(Locale.US);
1✔
102
    // hostName and pattern are now in lower case -- domain names are case-insensitive.
103

104
    // Strip trailing dot to normalize FQDN (e.g. "example.com.") to a relative form,
105
    // as per RFC 1034 Section 3.1 the two are semantically equivalent.
106
    if (hostName.endsWith(".")) {
1✔
107
      hostName = hostName.substring(0, hostName.length() - 1);
1✔
108
    }
109
    if (pattern.endsWith(".")) {
1✔
110
      pattern = pattern.substring(0, pattern.length() - 1);
1✔
111
    }
112

113
    if (!pattern.contains("*")) {
1✔
114
      // Not a wildcard pattern -- hostName and pattern must match exactly.
115
      return hostName.equals(pattern);
1✔
116
    }
117
    // Wildcard pattern
118

119
    if (pattern.length() == 1) {
1✔
120
      return true;
1✔
121
    }
122

123
    int index = pattern.indexOf('*');
1✔
124

125
    // At most one asterisk (*) is allowed.
126
    if (pattern.indexOf('*', index + 1) != -1) {
1✔
127
      return false;
×
128
    }
129

130
    // Asterisk can only match prefix or suffix.
131
    if (index != 0 && index != pattern.length() - 1) {
1✔
132
      return false;
×
133
    }
134

135
    // HostName must be at least as long as the pattern because asterisk has to
136
    // match one or more characters.
137
    if (hostName.length() < pattern.length()) {
1✔
138
      return false;
1✔
139
    }
140

141
    if (index == 0 && hostName.endsWith(pattern.substring(1))) {
1✔
142
      // Prefix matching fails.
143
      return true;
1✔
144
    }
145

146
    // Pattern matches hostname if suffix matching succeeds.
147
    return index == pattern.length() - 1
1✔
148
        && hostName.startsWith(pattern.substring(0, pattern.length() - 1));
1✔
149
  }
150

151
  /**
152
   * Returns {@code true} iff the given {@link RouteMatch} matches the RPC's full method name and
153
   * headers.
154
   */
155
  static boolean matchRoute(RouteMatch routeMatch, String fullMethodName,
156
      Metadata headers, ThreadSafeRandom random) {
157
    if (!matchPath(routeMatch.pathMatcher(), fullMethodName)) {
1✔
158
      return false;
1✔
159
    }
160
    for (HeaderMatcher headerMatcher : routeMatch.headerMatchers()) {
1✔
161
      if (!headerMatcher.matches(getHeaderValue(headers, headerMatcher.name()))) {
1✔
162
        return false;
1✔
163
      }
164
    }
1✔
165
    FractionMatcher fraction = routeMatch.fractionMatcher();
1✔
166
    return fraction == null || random.nextInt(fraction.denominator()) < fraction.numerator();
1✔
167
  }
168

169
  private static boolean matchPath(PathMatcher pathMatcher, String fullMethodName) {
170
    if (pathMatcher.path() != null) {
1✔
171
      return pathMatcher.caseSensitive()
1✔
172
          ? pathMatcher.path().equals(fullMethodName)
1✔
173
          : pathMatcher.path().equalsIgnoreCase(fullMethodName);
1✔
174
    } else if (pathMatcher.prefix() != null) {
1✔
175
      return pathMatcher.caseSensitive()
1✔
176
          ? fullMethodName.startsWith(pathMatcher.prefix())
1✔
177
          : fullMethodName.toLowerCase(Locale.US).startsWith(
1✔
178
              pathMatcher.prefix().toLowerCase(Locale.US));
1✔
179
    }
180
    return pathMatcher.regEx().matches(fullMethodName);
1✔
181
  }
182

183
  @Nullable
184
  private static String getHeaderValue(Metadata headers, String headerName) {
185
    if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
186
      return null;
×
187
    }
188
    if (headerName.equals("content-type")) {
1✔
189
      return "application/grpc";
1✔
190
    }
191
    Metadata.Key<String> key;
192
    try {
193
      key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
1✔
194
    } catch (IllegalArgumentException e) {
1✔
195
      return null;
1✔
196
    }
1✔
197
    Iterable<String> values = headers.getAll(key);
1✔
198
    return values == null ? null : Joiner.on(",").join(values);
1✔
199
  }
200
}
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