diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java index 4f2a1ec2a9..264a36ba39 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -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; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java index ea40eee663..fff8f04f71 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java @@ -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; diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java index 29d4c3c140..0dd8655321 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java @@ -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 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 parse(File... etcResolverFiles) throws IOException { Map 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 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 domainToNameServerStreamMap, String domainName, - List addresses) { + List 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 domainToNameServerStreamMap, @@ -266,25 +277,25 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ } /** - * Parse a file of the format /etc/resolv.conf 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 /etc/resolv.conf 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 /etc/resolv.conf and return the - * value corresponding to the first ndots in an options configuration. + * Parse a file of the format /etc/resolv.conf and return options + * of interest, namely: timeout, attempts and ndots. * @param etcResolvConf a file of the format /etc/resolv.conf. - * @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; } + } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java new file mode 100644 index 0000000000..dcfedd8baa --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java @@ -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 etc/resolv.conf. + */ +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); + } + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java index 6bb133a11a..97e3d8a75f 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java @@ -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