Dns resolver: honor resolv.conf timeout, rotate and attempts options (#10207)

Motivations
-----------
DnsNameResolverBuilder and DnsNameResolver do not auto-configure
themselves uing default options define in /etc/resolv.conf.
In particular, rotate, timeout and attempts options are ignored.

Modifications
-------------
 - Modified UnixResolverDnsServerAddressStreamProvider to parse ndots,
attempts and timeout options all at once and use these defaults to
configure DnsNameResolver when values are not provided by the
DnsNameResolverBuilder.
 - When rotate option is specified, the DnsServerAddresses returned by
UnixResolverDnsServerAddressStreamProvider is rotational.
 - Amend resolv.conf options with the RES_OPTIONS environment variable
when present.

Result:

Fixes https://github.com/netty/netty/issues/10202
This commit is contained in:
Fabien Renaud 2020-04-28 00:28:05 -07:00 committed by Norman Maurer
parent 391cdcdd77
commit 0c7a01503a
5 changed files with 271 additions and 53 deletions

View File

@ -77,9 +77,9 @@ import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.parseEtcResolverFirstNdots;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static java.util.Objects.requireNonNull;
@ -111,7 +111,7 @@ public class DnsNameResolver extends InetNameResolver {
static final ResolvedAddressTypes DEFAULT_RESOLVE_ADDRESS_TYPES;
static final String[] DEFAULT_SEARCH_DOMAINS;
private static final int DEFAULT_NDOTS;
private static final UnixResolverOptions DEFAULT_OPTIONS;
static {
if (NetUtil.isIpV4StackPreferred() || !anyInterfaceSupportsIpV6()) {
@ -141,13 +141,13 @@ public class DnsNameResolver extends InetNameResolver {
}
DEFAULT_SEARCH_DOMAINS = searchDomains;
int ndots;
UnixResolverOptions options;
try {
ndots = parseEtcResolverFirstNdots();
options = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverOptions();
} catch (Exception ignore) {
ndots = UnixResolverDnsServerAddressStreamProvider.DEFAULT_NDOTS;
options = UnixResolverOptions.newBuilder().build();
}
DEFAULT_NDOTS = ndots;
DEFAULT_OPTIONS = options;
}
/**
@ -383,10 +383,12 @@ public class DnsNameResolver extends InetNameResolver {
boolean decodeIdn,
boolean completeOncePreferredResolved) {
super(eventLoop);
this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis");
this.queryTimeoutMillis = queryTimeoutMillis > 0
? queryTimeoutMillis
: TimeUnit.SECONDS.toMillis(DEFAULT_OPTIONS.timeout());
this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES;
this.recursionDesired = recursionDesired;
this.maxQueriesPerResolve = checkPositive(maxQueriesPerResolve, "maxQueriesPerResolve");
this.maxQueriesPerResolve = maxQueriesPerResolve > 0 ? maxQueriesPerResolve : DEFAULT_OPTIONS.attempts();
this.maxPayloadSize = checkPositive(maxPayloadSize, "maxPayloadSize");
this.optResourceEnabled = optResourceEnabled;
this.hostsFileEntriesResolver = requireNonNull(hostsFileEntriesResolver, "hostsFileEntriesResolver");
@ -401,7 +403,7 @@ public class DnsNameResolver extends InetNameResolver {
dnsQueryLifecycleObserverFactory) :
requireNonNull(dnsQueryLifecycleObserverFactory, "dnsQueryLifecycleObserverFactory");
this.searchDomains = searchDomains != null ? searchDomains.clone() : DEFAULT_SEARCH_DOMAINS;
this.ndots = ndots >= 0 ? ndots : DEFAULT_NDOTS;
this.ndots = ndots >= 0 ? ndots : DEFAULT_OPTIONS.ndots();
this.decodeIdn = decodeIdn;
this.completeOncePreferredResolved = completeOncePreferredResolved;
this.socketChannelFactory = socketChannelFactory;

View File

@ -29,7 +29,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault;
import static io.netty.util.internal.ObjectUtil.intValue;
import static java.util.Objects.requireNonNull;
@ -46,16 +45,17 @@ public final class DnsNameResolverBuilder {
private Integer minTtl;
private Integer maxTtl;
private Integer negativeTtl;
private long queryTimeoutMillis = 5000;
private long queryTimeoutMillis = -1;
private ResolvedAddressTypes resolvedAddressTypes = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES;
private boolean completeOncePreferredResolved;
private boolean recursionDesired = true;
private int maxQueriesPerResolve = 16;
private int maxQueriesPerResolve = -1;
private boolean traceEnabled;
private int maxPayloadSize = 4096;
private boolean optResourceEnabled = true;
private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT;
private DnsServerAddressStreamProvider dnsServerAddressStreamProvider = platformDefault();
private DnsServerAddressStreamProvider dnsServerAddressStreamProvider =
DnsServerAddressStreamProviders.platformDefault();
private DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory =
NoopDnsQueryLifecycleObserverFactory.INSTANCE;
private String[] searchDomains;

View File

@ -47,19 +47,22 @@ import static java.util.Objects.requireNonNull;
public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
private static final InternalLogger logger =
InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class);
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final String RES_OPTIONS = System.getenv("RES_OPTIONS");
private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf";
private static final String ETC_RESOLVER_DIR = "/etc/resolver";
private static final String NAMESERVER_ROW_LABEL = "nameserver";
private static final String SORTLIST_ROW_LABEL = "sortlist";
private static final String OPTIONS_ROW_LABEL = "options";
private static final String OPTIONS_ROW_LABEL = "options ";
private static final String OPTIONS_ROTATE_FLAG = "rotate";
private static final String DOMAIN_ROW_LABEL = "domain";
private static final String SEARCH_ROW_LABEL = "search";
private static final String PORT_ROW_LABEL = "port";
private static final String NDOTS_LABEL = "ndots:";
static final int DEFAULT_NDOTS = 1;
private final DnsServerAddresses defaultNameServerAddresses;
private final Map<String, DnsServerAddresses> domainToNameServerStreamMap;
private static final Pattern SEARCH_DOMAIN_PATTERN = Pattern.compile("\\s+");
/**
* Attempt to parse {@code /etc/resolv.conf} and files in the {@code /etc/resolver} directory by default.
@ -155,6 +158,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
private static Map<String, DnsServerAddresses> parse(File... etcResolverFiles) throws IOException {
Map<String, DnsServerAddresses> domainToNameServerStreamMap =
new HashMap<>(etcResolverFiles.length << 1);
boolean rotateGlobal = RES_OPTIONS != null && RES_OPTIONS.contains(OPTIONS_ROTATE_FLAG);
for (File etcResolverFile : etcResolverFiles) {
if (!etcResolverFile.isFile()) {
continue;
@ -165,6 +169,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
br = new BufferedReader(fr);
List<InetSocketAddress> addresses = new ArrayList<>(2);
String domainName = etcResolverFile.getName();
boolean rotate = rotateGlobal;
int port = DNS_PORT;
String line;
while ((line = br.readLine()) != null) {
@ -174,7 +179,9 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
continue;
}
if (line.startsWith(NAMESERVER_ROW_LABEL)) {
if (!rotate && line.startsWith(OPTIONS_ROW_LABEL)) {
rotate = line.contains(OPTIONS_ROTATE_FLAG);
} else if (line.startsWith(NAMESERVER_ROW_LABEL)) {
int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
if (i < 0) {
throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
@ -214,7 +221,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
}
domainName = line.substring(i);
if (!addresses.isEmpty()) {
putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
}
addresses = new ArrayList<>(2);
} else if (line.startsWith(PORT_ROW_LABEL)) {
@ -232,7 +239,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
}
}
if (!addresses.isEmpty()) {
putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
}
} finally {
if (br == null) {
@ -247,9 +254,13 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
String domainName,
List<InetSocketAddress> addresses) {
List<InetSocketAddress> addresses,
boolean rotate) {
// TODO(scott): sortlist is being ignored.
putIfAbsent(domainToNameServerStreamMap, domainName, DnsServerAddresses.sequential(addresses));
DnsServerAddresses addrs = rotate
? DnsServerAddresses.rotational(addresses)
: DnsServerAddresses.sequential(addresses);
putIfAbsent(domainToNameServerStreamMap, domainName, addrs);
}
private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
@ -266,25 +277,25 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
}
/**
* Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
* value corresponding to the first ndots in an options configuration.
* @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not
* found.
* Parse <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return options of interest, namely:
* timeout, attempts and ndots.
* @return The options values provided by /etc/resolve.conf.
* @throws IOException If a failure occurs parsing the file.
*/
static int parseEtcResolverFirstNdots() throws IOException {
return parseEtcResolverFirstNdots(new File(ETC_RESOLV_CONF_FILE));
static UnixResolverOptions parseEtcResolverOptions() throws IOException {
return parseEtcResolverOptions(new File(ETC_RESOLV_CONF_FILE));
}
/**
* Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
* value corresponding to the first ndots in an options configuration.
* Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return options
* of interest, namely: timeout, attempts and ndots.
* @param etcResolvConf a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
* @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not
* found.
* @return The options values provided by /etc/resolve.conf.
* @throws IOException If a failure occurs parsing the file.
*/
static int parseEtcResolverFirstNdots(File etcResolvConf) throws IOException {
static UnixResolverOptions parseEtcResolverOptions(File etcResolvConf) throws IOException {
UnixResolverOptions.Builder optionsBuilder = UnixResolverOptions.newBuilder();
FileReader fr = new FileReader(etcResolvConf);
BufferedReader br = null;
try {
@ -292,12 +303,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith(OPTIONS_ROW_LABEL)) {
int i = line.indexOf(NDOTS_LABEL);
if (i >= 0) {
i += NDOTS_LABEL.length();
final int j = line.indexOf(' ', i);
return Integer.parseInt(line.substring(i, j < 0 ? line.length() : j));
}
parseResOptions(line.substring(OPTIONS_ROW_LABEL.length()), optionsBuilder);
break;
}
}
@ -308,7 +314,35 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
br.close();
}
}
return DEFAULT_NDOTS;
// amend options
if (RES_OPTIONS != null) {
parseResOptions(RES_OPTIONS, optionsBuilder);
}
return optionsBuilder.build();
}
private static void parseResOptions(String line, UnixResolverOptions.Builder builder) {
String[] opts = WHITESPACE_PATTERN.split(line);
for (String opt : opts) {
try {
if (opt.startsWith("ndots:")) {
builder.setNdots(parseResIntOption(opt, "ndots:"));
} else if (opt.startsWith("attempts:")) {
builder.setAttempts(parseResIntOption(opt, "attempts:"));
} else if (opt.startsWith("timeout:")) {
builder.setTimeout(parseResIntOption(opt, "timeout:"));
}
} catch (NumberFormatException ignore) {
// skip bad int values from resolv.conf to keep value already set in UnixResolverOptions
}
}
}
private static int parseResIntOption(String opt, String fullLabel) {
String optValue = opt.substring(fullLabel.length());
return Integer.parseInt(optValue);
}
/**
@ -348,7 +382,7 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
if (i >= 0) {
// May contain more then one entry, either seperated by whitespace or tab.
// See https://linux.die.net/man/5/resolver
String[] domains = SEARCH_DOMAIN_PATTERN.split(line.substring(i));
String[] domains = WHITESPACE_PATTERN.split(line.substring(i));
Collections.addAll(searchDomains, domains);
}
}
@ -366,4 +400,5 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
? Collections.singletonList(localDomain)
: searchDomains;
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2020 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.resolver.dns;
/**
* Represents options defined in a file of the format <a href=https://linux.die.net/man/5/resolver>etc/resolv.conf</a>.
*/
final class UnixResolverOptions {
private final int ndots;
private final int timeout;
private final int attempts;
UnixResolverOptions(int ndots, int timeout, int attempts) {
this.ndots = ndots;
this.timeout = timeout;
this.attempts = attempts;
}
static UnixResolverOptions.Builder newBuilder() {
return new UnixResolverOptions.Builder();
}
/**
* The number of dots which must appear in a name before an initial absolute query is made.
* The default value is {@code 1}.
*/
int ndots() {
return ndots;
}
/**
* The timeout of each DNS query performed by this resolver (in seconds).
* The default value is {@code 5}.
*/
int timeout() {
return timeout;
}
/**
* The maximum allowed number of DNS queries to send when resolving a host name.
* The default value is {@code 16}.
*/
int attempts() {
return attempts;
}
static final class Builder {
private int ndots = 1;
private int timeout = 5;
private int attempts = 16;
private Builder() {
}
void setNdots(int ndots) {
this.ndots = ndots;
}
void setTimeout(int timeout) {
this.timeout = timeout;
}
void setAttempts(int attempts) {
this.attempts = attempts;
}
UnixResolverOptions build() {
return new UnixResolverOptions(ndots, timeout, attempts);
}
}
}

View File

@ -29,8 +29,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.DEFAULT_NDOTS;
import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.parseEtcResolverFirstNdots;
import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.parseEtcResolverOptions;
import static org.junit.Assert.assertEquals;
public class UnixResolverDnsServerAddressStreamProviderTest {
@ -50,6 +49,62 @@ public class UnixResolverDnsServerAddressStreamProviderTest {
assertHostNameEquals("127.0.0.3", stream.next());
}
@Test
public void nameServerAddressStreamShouldBeRotationalWhenRotationOptionsIsPresent() throws Exception {
File f = buildFile("options rotate\n" +
"domain linecorp.local\n" +
"nameserver 127.0.0.2\n" +
"nameserver 127.0.0.3\n" +
"nameserver 127.0.0.4\n");
UnixResolverDnsServerAddressStreamProvider p =
new UnixResolverDnsServerAddressStreamProvider(f, null);
DnsServerAddressStream stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
assertHostNameEquals("127.0.0.2", stream.next());
stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.4", stream.next());
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
}
@Test
public void nameServerAddressStreamShouldAlwaysStartFromTheTopWhenRotationOptionsIsAbsent() throws Exception {
File f = buildFile("domain linecorp.local\n" +
"nameserver 127.0.0.2\n" +
"nameserver 127.0.0.3\n" +
"nameserver 127.0.0.4\n");
UnixResolverDnsServerAddressStreamProvider p =
new UnixResolverDnsServerAddressStreamProvider(f, null);
DnsServerAddressStream stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
stream = p.nameServerAddressStream("");
assertHostNameEquals("127.0.0.2", stream.next());
assertHostNameEquals("127.0.0.3", stream.next());
assertHostNameEquals("127.0.0.4", stream.next());
}
@Test
public void defaultReturnedWhenNoBetterMatch() throws Exception {
File f = buildFile("domain linecorp.local\n" +
@ -83,23 +138,63 @@ public class UnixResolverDnsServerAddressStreamProviderTest {
}
@Test
public void ndotsIsParsedIfPresent() throws IOException {
public void ndotsOptionIsParsedIfPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options ndots:0\n");
assertEquals(0, parseEtcResolverFirstNdots(f));
"nameserver 127.0.0.11\n" +
"options ndots:0\n");
assertEquals(0, parseEtcResolverOptions(f).ndots());
f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options ndots:123 foo:goo\n");
assertEquals(123, parseEtcResolverFirstNdots(f));
"nameserver 127.0.0.11\n" +
"options ndots:123 foo:goo\n");
assertEquals(123, parseEtcResolverOptions(f).ndots());
}
@Test
public void defaultValueReturnedIfNdotsNotPresent() throws IOException {
public void defaultValueReturnedIfNdotsOptionsNotPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n");
assertEquals(DEFAULT_NDOTS, parseEtcResolverFirstNdots(f));
"nameserver 127.0.0.11\n");
assertEquals(1, parseEtcResolverOptions(f).ndots());
}
@Test
public void timeoutOptionIsParsedIfPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options timeout:0\n");
assertEquals(0, parseEtcResolverOptions(f).timeout());
f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options foo:bar timeout:124\n");
assertEquals(124, parseEtcResolverOptions(f).timeout());
}
@Test
public void defaultValueReturnedIfTimeoutOptionsIsNotPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n");
assertEquals(5, parseEtcResolverOptions(f).timeout());
}
@Test
public void attemptsOptionIsParsedIfPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options attempts:0\n");
assertEquals(0, parseEtcResolverOptions(f).attempts());
f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n" +
"options foo:bar attempts:12\n");
assertEquals(12, parseEtcResolverOptions(f).attempts());
}
@Test
public void defaultValueReturnedIfAttemptsOptionsIsNotPresent() throws IOException {
File f = buildFile("search localdomain\n" +
"nameserver 127.0.0.11\n");
assertEquals(16, parseEtcResolverOptions(f).attempts());
}
@Test