mirror of https://github.com/grpc/grpc-java.git
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 #4912
This commit is contained in:
parent
e8762c941c
commit
e5339d25c6
|
|
@ -86,7 +86,7 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
if (unavailabilityCause() != null) {
|
if (unavailabilityCause() != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new JndiResourceResolver();
|
return new JndiResourceResolver(new JndiRecordFetcher());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -95,6 +95,11 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
return JNDI_UNAVAILABILITY_CAUSE;
|
return JNDI_UNAVAILABILITY_CAUSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
interface RecordFetcher {
|
||||||
|
List<String> getAllRecords(String recordType, String name) throws NamingException;
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final class JndiResourceResolver implements DnsNameResolver.ResourceResolver {
|
static final class JndiResourceResolver implements DnsNameResolver.ResourceResolver {
|
||||||
private static final Logger logger =
|
private static final Logger logger =
|
||||||
|
|
@ -102,15 +107,20 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
|
|
||||||
private static final Pattern whitespace = Pattern.compile("\\s+");
|
private static final Pattern whitespace = Pattern.compile("\\s+");
|
||||||
|
|
||||||
|
private final RecordFetcher recordFetcher;
|
||||||
|
|
||||||
|
public JndiResourceResolver(RecordFetcher recordFetcher) {
|
||||||
|
this.recordFetcher = recordFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> resolveTxt(String serviceConfigHostname) throws NamingException {
|
public List<String> resolveTxt(String serviceConfigHostname) throws NamingException {
|
||||||
checkAvailable();
|
|
||||||
if (logger.isLoggable(Level.FINER)) {
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
logger.log(
|
logger.log(
|
||||||
Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname});
|
Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname});
|
||||||
}
|
}
|
||||||
List<String> serviceConfigRawTxtRecords =
|
List<String> serviceConfigRawTxtRecords =
|
||||||
getAllRecords("TXT", "dns:///" + serviceConfigHostname);
|
recordFetcher.getAllRecords("TXT", "dns:///" + serviceConfigHostname);
|
||||||
if (logger.isLoggable(Level.FINER)) {
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
logger.log(
|
logger.log(
|
||||||
Level.FINER, "Found {0} TXT records", new Object[]{serviceConfigRawTxtRecords.size()});
|
Level.FINER, "Found {0} TXT records", new Object[]{serviceConfigRawTxtRecords.size()});
|
||||||
|
|
@ -126,13 +136,12 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
@Override
|
@Override
|
||||||
public List<EquivalentAddressGroup> resolveSrv(
|
public List<EquivalentAddressGroup> resolveSrv(
|
||||||
AddressResolver addressResolver, String grpclbHostname) throws Exception {
|
AddressResolver addressResolver, String grpclbHostname) throws Exception {
|
||||||
checkAvailable();
|
|
||||||
if (logger.isLoggable(Level.FINER)) {
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
logger.log(
|
logger.log(
|
||||||
Level.FINER, "About to query SRV records for {0}", new Object[]{grpclbHostname});
|
Level.FINER, "About to query SRV records for {0}", new Object[]{grpclbHostname});
|
||||||
}
|
}
|
||||||
List<String> grpclbSrvRecords =
|
List<String> grpclbSrvRecords =
|
||||||
getAllRecords("SRV", "dns:///" + grpclbHostname);
|
recordFetcher.getAllRecords("SRV", "dns:///" + grpclbHostname);
|
||||||
if (logger.isLoggable(Level.FINER)) {
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
logger.log(
|
logger.log(
|
||||||
Level.FINER, "Found {0} SRV records", new Object[]{grpclbSrvRecords.size()});
|
Level.FINER, "Found {0} SRV records", new Object[]{grpclbSrvRecords.size()});
|
||||||
|
|
@ -144,14 +153,23 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
for (String srvRecord : grpclbSrvRecords) {
|
for (String srvRecord : grpclbSrvRecords) {
|
||||||
try {
|
try {
|
||||||
SrvRecord record = parseSrvRecord(srvRecord);
|
SrvRecord record = parseSrvRecord(srvRecord);
|
||||||
|
// SRV requires the host name to be absolute
|
||||||
|
if (!record.host.endsWith(".")) {
|
||||||
|
throw new RuntimeException("Returned SRV host does not end in period: " + record.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip trailing dot for appearance's sake. It _should_ be fine either way, but most
|
||||||
|
// people expect to see it without the dot.
|
||||||
|
String authority = record.host.substring(0, record.host.length() - 1);
|
||||||
|
// But we want to use the trailing dot for the IP lookup. The dot makes the name absolute
|
||||||
|
// instead of relative and so will avoid the search list like that in resolv.conf.
|
||||||
List<? extends InetAddress> addrs = addressResolver.resolveAddress(record.host);
|
List<? extends InetAddress> addrs = addressResolver.resolveAddress(record.host);
|
||||||
List<SocketAddress> sockaddrs = new ArrayList<>(addrs.size());
|
List<SocketAddress> sockaddrs = new ArrayList<>(addrs.size());
|
||||||
for (InetAddress addr : addrs) {
|
for (InetAddress addr : addrs) {
|
||||||
sockaddrs.add(new InetSocketAddress(addr, record.port));
|
sockaddrs.add(new InetSocketAddress(addr, record.port));
|
||||||
}
|
}
|
||||||
Attributes attrs = Attributes.newBuilder()
|
Attributes attrs = Attributes.newBuilder()
|
||||||
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, record.host)
|
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, authority)
|
||||||
.build();
|
.build();
|
||||||
balancerAddresses.add(
|
balancerAddresses.add(
|
||||||
new EquivalentAddressGroup(Collections.unmodifiableList(sockaddrs), attrs));
|
new EquivalentAddressGroup(Collections.unmodifiableList(sockaddrs), attrs));
|
||||||
|
|
@ -176,8 +194,7 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
return Collections.unmodifiableList(balancerAddresses);
|
return Collections.unmodifiableList(balancerAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
private static final class SrvRecord {
|
||||||
static final class SrvRecord {
|
|
||||||
SrvRecord(String host, int port) {
|
SrvRecord(String host, int port) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
@ -187,17 +204,50 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
final int port;
|
final int port;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
@SuppressWarnings("BetaApi") // Verify is only kinda beta
|
@SuppressWarnings("BetaApi") // Verify is only kinda beta
|
||||||
static SrvRecord parseSrvRecord(String rawRecord) {
|
private static SrvRecord parseSrvRecord(String rawRecord) {
|
||||||
String[] parts = whitespace.split(rawRecord);
|
String[] parts = whitespace.split(rawRecord);
|
||||||
Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawRecord);
|
Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawRecord);
|
||||||
return new SrvRecord(parts[3], Integer.parseInt(parts[2]));
|
return new SrvRecord(parts[3], Integer.parseInt(parts[2]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoreJRERequirement
|
/**
|
||||||
private static List<String> getAllRecords(String recordType, String name)
|
* Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
|
||||||
throws NamingException {
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String unquote(String txtRecord) {
|
||||||
|
StringBuilder sb = new StringBuilder(txtRecord.length());
|
||||||
|
boolean inquote = false;
|
||||||
|
for (int i = 0; i < txtRecord.length(); i++) {
|
||||||
|
char c = txtRecord.charAt(i);
|
||||||
|
if (!inquote) {
|
||||||
|
if (c == ' ') {
|
||||||
|
continue;
|
||||||
|
} else if (c == '"') {
|
||||||
|
inquote = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (c == '"') {
|
||||||
|
inquote = false;
|
||||||
|
continue;
|
||||||
|
} else if (c == '\\') {
|
||||||
|
c = txtRecord.charAt(++i);
|
||||||
|
assert c == '"' || c == '\\';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@IgnoreJRERequirement
|
||||||
|
static final class JndiRecordFetcher implements RecordFetcher {
|
||||||
|
@Override
|
||||||
|
public List<String> getAllRecords(String recordType, String name) throws NamingException {
|
||||||
|
checkAvailable();
|
||||||
String[] rrType = new String[]{recordType};
|
String[] rrType = new String[]{recordType};
|
||||||
List<String> records = new ArrayList<>();
|
List<String> records = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -237,7 +287,6 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoreJRERequirement
|
|
||||||
private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)
|
private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)
|
||||||
throws NamingException {
|
throws NamingException {
|
||||||
try {
|
try {
|
||||||
|
|
@ -248,7 +297,6 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoreJRERequirement
|
|
||||||
private static void closeThenThrow(DirContext ctx, NamingException e) throws NamingException {
|
private static void closeThenThrow(DirContext ctx, NamingException e) throws NamingException {
|
||||||
try {
|
try {
|
||||||
ctx.close();
|
ctx.close();
|
||||||
|
|
@ -258,36 +306,6 @@ final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResol
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
static String unquote(String txtRecord) {
|
|
||||||
StringBuilder sb = new StringBuilder(txtRecord.length());
|
|
||||||
boolean inquote = false;
|
|
||||||
for (int i = 0; i < txtRecord.length(); i++) {
|
|
||||||
char c = txtRecord.charAt(i);
|
|
||||||
if (!inquote) {
|
|
||||||
if (c == ' ') {
|
|
||||||
continue;
|
|
||||||
} else if (c == '"') {
|
|
||||||
inquote = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (c == '"') {
|
|
||||||
inquote = false;
|
|
||||||
continue;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
c = txtRecord.charAt(++i);
|
|
||||||
assert c == '"' || c == '\\';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkAvailable() {
|
private static void checkAvailable() {
|
||||||
if (JNDI_UNAVAILABILITY_CAUSE != null) {
|
if (JNDI_UNAVAILABILITY_CAUSE != null) {
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,21 @@ package io.grpc.internal;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.EquivalentAddressGroup;
|
||||||
import io.grpc.internal.DnsNameResolver.AddressResolver;
|
import io.grpc.internal.DnsNameResolver.AddressResolver;
|
||||||
|
import io.grpc.internal.GrpcAttributes;
|
||||||
|
import io.grpc.internal.JndiResourceResolverFactory.JndiRecordFetcher;
|
||||||
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver;
|
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver;
|
||||||
import io.grpc.internal.JndiResourceResolverFactory.JndiResourceResolver.SrvRecord;
|
import io.grpc.internal.JndiResourceResolverFactory.RecordFetcher;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -50,15 +60,9 @@ public class JndiResourceResolverTest {
|
||||||
public void jndiResolverWorks() throws Exception {
|
public void jndiResolverWorks() throws Exception {
|
||||||
Assume.assumeNoException(new JndiResourceResolverFactory().unavailabilityCause());
|
Assume.assumeNoException(new JndiResourceResolverFactory().unavailabilityCause());
|
||||||
|
|
||||||
AddressResolver addressResolver = new AddressResolver() {
|
RecordFetcher recordFetcher = new JndiRecordFetcher();
|
||||||
@Override
|
|
||||||
public List<InetAddress> resolveAddress(String host) throws Exception {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
JndiResourceResolver resolver = new JndiResourceResolver();
|
|
||||||
try {
|
try {
|
||||||
resolver.resolveSrv(addressResolver, "localhost");
|
recordFetcher.getAllRecords("SRV", "dns:///localhost");
|
||||||
} catch (javax.naming.CommunicationException e) {
|
} catch (javax.naming.CommunicationException e) {
|
||||||
Assume.assumeNoException(e);
|
Assume.assumeNoException(e);
|
||||||
} catch (javax.naming.NameNotFoundException e) {
|
} catch (javax.naming.NameNotFoundException e) {
|
||||||
|
|
@ -67,9 +71,45 @@ public class JndiResourceResolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseSrvRecord() {
|
public void txtRecordLookup() throws Exception {
|
||||||
SrvRecord record = JndiResourceResolver.parseSrvRecord("0 0 1234 foo.bar.com");
|
RecordFetcher recordFetcher = mock(RecordFetcher.class);
|
||||||
assertThat(record.host).isEqualTo("foo.bar.com");
|
when(recordFetcher.getAllRecords("TXT", "dns:///service.example.com"))
|
||||||
assertThat(record.port).isEqualTo(1234);
|
.thenReturn(Arrays.asList("foo", "\"bar\""));
|
||||||
|
|
||||||
|
List<String> golden = Arrays.asList("foo", "bar");
|
||||||
|
JndiResourceResolver resolver = new JndiResourceResolver(recordFetcher);
|
||||||
|
assertThat(resolver.resolveTxt("service.example.com")).isEqualTo(golden);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void srvRecordLookup() throws Exception {
|
||||||
|
AddressResolver addressResolver = mock(AddressResolver.class);
|
||||||
|
when(addressResolver.resolveAddress("foo.example.com."))
|
||||||
|
.thenReturn(Arrays.asList(InetAddress.getByName("127.1.2.3")));
|
||||||
|
when(addressResolver.resolveAddress("bar.example.com."))
|
||||||
|
.thenReturn(Arrays.asList(
|
||||||
|
InetAddress.getByName("127.3.2.1"), InetAddress.getByName("::1")));
|
||||||
|
when(addressResolver.resolveAddress("unknown.example.com."))
|
||||||
|
.thenThrow(new UnknownHostException("unknown.example.com."));
|
||||||
|
RecordFetcher recordFetcher = mock(RecordFetcher.class);
|
||||||
|
when(recordFetcher.getAllRecords("SRV", "dns:///service.example.com"))
|
||||||
|
.thenReturn(Arrays.asList(
|
||||||
|
"0 0 314 foo.example.com.", "0 0 42 bar.example.com.", "0 0 1 unknown.example.com."));
|
||||||
|
|
||||||
|
List<EquivalentAddressGroup> golden = Arrays.asList(
|
||||||
|
new EquivalentAddressGroup(
|
||||||
|
Arrays.<SocketAddress>asList(new InetSocketAddress("127.1.2.3", 314)),
|
||||||
|
Attributes.newBuilder()
|
||||||
|
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, "foo.example.com")
|
||||||
|
.build()),
|
||||||
|
new EquivalentAddressGroup(
|
||||||
|
Arrays.<SocketAddress>asList(
|
||||||
|
new InetSocketAddress("127.3.2.1", 42),
|
||||||
|
new InetSocketAddress("::1", 42)),
|
||||||
|
Attributes.newBuilder()
|
||||||
|
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, "bar.example.com")
|
||||||
|
.build()));
|
||||||
|
JndiResourceResolver resolver = new JndiResourceResolver(recordFetcher);
|
||||||
|
assertThat(resolver.resolveSrv(addressResolver, "service.example.com")).isEqualTo(golden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue