Skip to content

Commit e5339d2

Browse files
committed
core: Trim trailing dot from SRV hostnames
The trailing dot denotes the hostname to be absolute. It is fine to leave, but removing it makes the authority match the more common form and hopefully reduces confusion. This happens to works around SNI failures caused when using gRPC-LB, since SNI prohibits the trailing dot. However, that is not the reason for this change as we have to support users directly providing a hostname with the trailing dot anyway (and doing so is not hard). See grpc#4912
1 parent e8762c9 commit e5339d2

File tree

2 files changed

+116
-58
lines changed

2 files changed

+116
-58
lines changed

core/src/main/java/io/grpc/internal/JndiResourceResolverFactory.java

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public ResourceResolver newResourceResolver() {
8686
if (unavailabilityCause() != null) {
8787
return null;
8888
}
89-
return new JndiResourceResolver();
89+
return new JndiResourceResolver(new JndiRecordFetcher());
9090
}
9191

9292
@Nullable
@@ -95,22 +95,32 @@ public Throwable unavailabilityCause() {
9595
return JNDI_UNAVAILABILITY_CAUSE;
9696
}
9797

98+
@VisibleForTesting
99+
interface RecordFetcher {
100+
List<String> getAllRecords(String recordType, String name) throws NamingException;
101+
}
102+
98103
@VisibleForTesting
99104
static final class JndiResourceResolver implements DnsNameResolver.ResourceResolver {
100105
private static final Logger logger =
101106
Logger.getLogger(JndiResourceResolver.class.getName());
102107

103108
private static final Pattern whitespace = Pattern.compile("\\s+");
104109

110+
private final RecordFetcher recordFetcher;
111+
112+
public JndiResourceResolver(RecordFetcher recordFetcher) {
113+
this.recordFetcher = recordFetcher;
114+
}
115+
105116
@Override
106117
public List<String> resolveTxt(String serviceConfigHostname) throws NamingException {
107-
checkAvailable();
108118
if (logger.isLoggable(Level.FINER)) {
109119
logger.log(
110120
Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname});
111121
}
112122
List<String> serviceConfigRawTxtRecords =
113-
getAllRecords("TXT", "dns:///" + serviceConfigHostname);
123+
recordFetcher.getAllRecords("TXT", "dns:///" + serviceConfigHostname);
114124
if (logger.isLoggable(Level.FINER)) {
115125
logger.log(
116126
Level.FINER, "Found {0} TXT records", new Object[]{serviceConfigRawTxtRecords.size()});
@@ -126,13 +136,12 @@ public List<String> resolveTxt(String serviceConfigHostname) throws NamingExcept
126136
@Override
127137
public List<EquivalentAddressGroup> resolveSrv(
128138
AddressResolver addressResolver, String grpclbHostname) throws Exception {
129-
checkAvailable();
130139
if (logger.isLoggable(Level.FINER)) {
131140
logger.log(
132141
Level.FINER, "About to query SRV records for {0}", new Object[]{grpclbHostname});
133142
}
134143
List<String> grpclbSrvRecords =
135-
getAllRecords("SRV", "dns:///" + grpclbHostname);
144+
recordFetcher.getAllRecords("SRV", "dns:///" + grpclbHostname);
136145
if (logger.isLoggable(Level.FINER)) {
137146
logger.log(
138147
Level.FINER, "Found {0} SRV records", new Object[]{grpclbSrvRecords.size()});
@@ -144,14 +153,23 @@ public List<EquivalentAddressGroup> resolveSrv(
144153
for (String srvRecord : grpclbSrvRecords) {
145154
try {
146155
SrvRecord record = parseSrvRecord(srvRecord);
156+
// SRV requires the host name to be absolute
157+
if (!record.host.endsWith(".")) {
158+
throw new RuntimeException("Returned SRV host does not end in period: " + record.host);
159+
}
147160

161+
// Strip trailing dot for appearance's sake. It _should_ be fine either way, but most
162+
// people expect to see it without the dot.
163+
String authority = record.host.substring(0, record.host.length() - 1);
164+
// But we want to use the trailing dot for the IP lookup. The dot makes the name absolute
165+
// instead of relative and so will avoid the search list like that in resolv.conf.
148166
List<? extends InetAddress> addrs = addressResolver.resolveAddress(record.host);
149167
List<SocketAddress> sockaddrs = new ArrayList<>(addrs.size());
150168
for (InetAddress addr : addrs) {
151169
sockaddrs.add(new InetSocketAddress(addr, record.port));
152170
}
153171
Attributes attrs = Attributes.newBuilder()
154-
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, record.host)
172+
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, authority)
155173
.build();
156174
balancerAddresses.add(
157175
new EquivalentAddressGroup(Collections.unmodifiableList(sockaddrs), attrs));
@@ -176,8 +194,7 @@ public List<EquivalentAddressGroup> resolveSrv(
176194
return Collections.unmodifiableList(balancerAddresses);
177195
}
178196

179-
@VisibleForTesting
180-
static final class SrvRecord {
197+
private static final class SrvRecord {
181198
SrvRecord(String host, int port) {
182199
this.host = host;
183200
this.port = port;
@@ -187,17 +204,50 @@ static final class SrvRecord {
187204
final int port;
188205
}
189206

190-
@VisibleForTesting
191207
@SuppressWarnings("BetaApi") // Verify is only kinda beta
192-
static SrvRecord parseSrvRecord(String rawRecord) {
208+
private static SrvRecord parseSrvRecord(String rawRecord) {
193209
String[] parts = whitespace.split(rawRecord);
194210
Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawRecord);
195211
return new SrvRecord(parts[3], Integer.parseInt(parts[2]));
196212
}
197213

198-
@IgnoreJRERequirement
199-
private static List<String> getAllRecords(String recordType, String name)
200-
throws NamingException {
214+
/**
215+
* Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
216+
*/
217+
@VisibleForTesting
218+
static String unquote(String txtRecord) {
219+
StringBuilder sb = new StringBuilder(txtRecord.length());
220+
boolean inquote = false;
221+
for (int i = 0; i < txtRecord.length(); i++) {
222+
char c = txtRecord.charAt(i);
223+
if (!inquote) {
224+
if (c == ' ') {
225+
continue;
226+
} else if (c == '"') {
227+
inquote = true;
228+
continue;
229+
}
230+
} else {
231+
if (c == '"') {
232+
inquote = false;
233+
continue;
234+
} else if (c == '\\') {
235+
c = txtRecord.charAt(++i);
236+
assert c == '"' || c == '\\';
237+
}
238+
}
239+
sb.append(c);
240+
}
241+
return sb.toString();
242+
}
243+
}
244+
245+
@VisibleForTesting
246+
@IgnoreJRERequirement
247+
static final class JndiRecordFetcher implements RecordFetcher {
248+
@Override
249+
public List<String> getAllRecords(String recordType, String name) throws NamingException {
250+
checkAvailable();
201251
String[] rrType = new String[]{recordType};
202252
List<String> records = new ArrayList<>();
203253

@@ -237,7 +287,6 @@ private static List<String> getAllRecords(String recordType, String name)
237287
return records;
238288
}
239289

240-
@IgnoreJRERequirement
241290
private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)
242291
throws NamingException {
243292
try {
@@ -248,7 +297,6 @@ private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, Namin
248297
throw e;
249298
}
250299

251-
@IgnoreJRERequirement
252300
private static void closeThenThrow(DirContext ctx, NamingException e) throws NamingException {
253301
try {
254302
ctx.close();
@@ -258,36 +306,6 @@ private static void closeThenThrow(DirContext ctx, NamingException e) throws Nam
258306
throw e;
259307
}
260308

261-
/**
262-
* Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
263-
*/
264-
@VisibleForTesting
265-
static String unquote(String txtRecord) {
266-
StringBuilder sb = new StringBuilder(txtRecord.length());
267-
boolean inquote = false;
268-
for (int i = 0; i < txtRecord.length(); i++) {
269-
char c = txtRecord.charAt(i);
270-
if (!inquote) {
271-
if (c == ' ') {
272-
continue;
273-
} else if (c == '"') {
274-
inquote = true;
275-
continue;
276-
}
277-
} else {
278-
if (c == '"') {
279-
inquote = false;
280-
continue;
281-
} else if (c == '\\') {
282-
c = txtRecord.charAt(++i);
283-
assert c == '"' || c == '\\';
284-
}
285-
}
286-
sb.append(c);
287-
}
288-
return sb.toString();
289-
}
290-
291309
private static void checkAvailable() {
292310
if (JNDI_UNAVAILABILITY_CAUSE != null) {
293311
throw new UnsupportedOperationException(

core/src/test/java/io/grpc/internal/JndiResourceResolverTest.java

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,21 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.when;
2123

24+
import io.grpc.Attributes;
25+
import io.grpc.EquivalentAddressGroup;
2226
import io.grpc.internal.DnsNameResolver.AddressResolver;
27+
import io.grpc.internal.GrpcAttributes;
28+
import io.grpc.internal.JndiResourceResolverFactory.JndiRecordFetcher;
2329
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver;
24-
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver.SrvRecord;
30+
import io.grpc.internal.JndiResourceResolverFactory.RecordFetcher;
2531
import java.net.InetAddress;
32+
import java.net.InetSocketAddress;
33+
import java.net.SocketAddress;
34+
import java.net.UnknownHostException;
35+
import java.util.Arrays;
2636
import java.util.List;
2737
import org.junit.Assume;
2838
import org.junit.Test;
@@ -50,15 +60,9 @@ public void normalizeDataRemovesJndiFormattingForTxtRecords() {
5060
public void jndiResolverWorks() throws Exception {
5161
Assume.assumeNoException(new JndiResourceResolverFactory().unavailabilityCause());
5262

53-
AddressResolver addressResolver = new AddressResolver() {
54-
@Override
55-
public List<InetAddress> resolveAddress(String host) throws Exception {
56-
return null;
57-
}
58-
};
59-
JndiResourceResolver resolver = new JndiResourceResolver();
63+
RecordFetcher recordFetcher = new JndiRecordFetcher();
6064
try {
61-
resolver.resolveSrv(addressResolver, "localhost");
65+
recordFetcher.getAllRecords("SRV", "dns:///localhost");
6266
} catch (javax.naming.CommunicationException e) {
6367
Assume.assumeNoException(e);
6468
} catch (javax.naming.NameNotFoundException e) {
@@ -67,9 +71,45 @@ public List<InetAddress> resolveAddress(String host) throws Exception {
6771
}
6872

6973
@Test
70-
public void parseSrvRecord() {
71-
SrvRecord record = JndiResourceResolver.parseSrvRecord("0 0 1234 foo.bar.com");
72-
assertThat(record.host).isEqualTo("foo.bar.com");
73-
assertThat(record.port).isEqualTo(1234);
74+
public void txtRecordLookup() throws Exception {
75+
RecordFetcher recordFetcher = mock(RecordFetcher.class);
76+
when(recordFetcher.getAllRecords("TXT", "dns:///service.example.com"))
77+
.thenReturn(Arrays.asList("foo", "\"bar\""));
78+
79+
List<String> golden = Arrays.asList("foo", "bar");
80+
JndiResourceResolver resolver = new JndiResourceResolver(recordFetcher);
81+
assertThat(resolver.resolveTxt("service.example.com")).isEqualTo(golden);
82+
}
83+
84+
@Test
85+
public void srvRecordLookup() throws Exception {
86+
AddressResolver addressResolver = mock(AddressResolver.class);
87+
when(addressResolver.resolveAddress("foo.example.com."))
88+
.thenReturn(Arrays.asList(InetAddress.getByName("127.1.2.3")));
89+
when(addressResolver.resolveAddress("bar.example.com."))
90+
.thenReturn(Arrays.asList(
91+
InetAddress.getByName("127.3.2.1"), InetAddress.getByName("::1")));
92+
when(addressResolver.resolveAddress("unknown.example.com."))
93+
.thenThrow(new UnknownHostException("unknown.example.com."));
94+
RecordFetcher recordFetcher = mock(RecordFetcher.class);
95+
when(recordFetcher.getAllRecords("SRV", "dns:///service.example.com"))
96+
.thenReturn(Arrays.asList(
97+
"0 0 314 foo.example.com.", "0 0 42 bar.example.com.", "0 0 1 unknown.example.com."));
98+
99+
List<EquivalentAddressGroup> golden = Arrays.asList(
100+
new EquivalentAddressGroup(
101+
Arrays.<SocketAddress>asList(new InetSocketAddress("127.1.2.3", 314)),
102+
Attributes.newBuilder()
103+
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, "foo.example.com")
104+
.build()),
105+
new EquivalentAddressGroup(
106+
Arrays.<SocketAddress>asList(
107+
new InetSocketAddress("127.3.2.1", 42),
108+
new InetSocketAddress("::1", 42)),
109+
Attributes.newBuilder()
110+
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, "bar.example.com")
111+
.build()));
112+
JndiResourceResolver resolver = new JndiResourceResolver(recordFetcher);
113+
assertThat(resolver.resolveSrv(addressResolver, "service.example.com")).isEqualTo(golden);
74114
}
75115
}

0 commit comments

Comments
 (0)