View Javadoc
1   /*
2    * Copyright 2017 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.resolver.dns;
17  
18  import io.netty.util.NetUtil;
19  import io.netty.util.internal.SocketUtils;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  
23  import java.io.BufferedReader;
24  import java.io.File;
25  import java.io.FileReader;
26  import java.io.IOException;
27  import java.net.InetSocketAddress;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.regex.Pattern;
35  
36  import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
37  import static io.netty.util.internal.ObjectUtil.checkNotNull;
38  import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace;
39  import static io.netty.util.internal.StringUtil.indexOfWhiteSpace;
40  
41  /**
42   * Able to parse files such as <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and
43   * <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
44   * /etc/resolver</a> to respect the system default domain servers.
45   */
46  public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
47      private static final InternalLogger logger =
48              InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class);
49  
50      private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
51      private static final String RES_OPTIONS = System.getenv("RES_OPTIONS");
52  
53      private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf";
54      private static final String ETC_RESOLVER_DIR = "/etc/resolver";
55      private static final String NAMESERVER_ROW_LABEL = "nameserver";
56      private static final String SORTLIST_ROW_LABEL = "sortlist";
57      private static final String OPTIONS_ROW_LABEL = "options ";
58      private static final String OPTIONS_ROTATE_FLAG = "rotate";
59      private static final String DOMAIN_ROW_LABEL = "domain";
60      private static final String SEARCH_ROW_LABEL = "search";
61      private static final String PORT_ROW_LABEL = "port";
62  
63      private final DnsServerAddresses defaultNameServerAddresses;
64      private final Map<String, DnsServerAddresses> domainToNameServerStreamMap;
65  
66      /**
67       * Attempt to parse {@code /etc/resolv.conf} and files in the {@code /etc/resolver} directory by default.
68       * A failure to parse will return {@link DefaultDnsServerAddressStreamProvider}.
69       */
70      static DnsServerAddressStreamProvider parseSilently() {
71          try {
72              UnixResolverDnsServerAddressStreamProvider nameServerCache =
73                      new UnixResolverDnsServerAddressStreamProvider(ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR);
74              return nameServerCache.mayOverrideNameServers() ? nameServerCache
75                                                              : DefaultDnsServerAddressStreamProvider.INSTANCE;
76          } catch (Exception e) {
77              if (logger.isDebugEnabled()) {
78                  logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e);
79              }
80              return DefaultDnsServerAddressStreamProvider.INSTANCE;
81          }
82      }
83  
84      /**
85       * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> which may contain
86       * the default DNS server to use, and also overrides for individual domains. Also parse list of files of the format
87       * <a href="
88       * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
89       * /etc/resolver</a> which may contain multiple files to override the name servers used for multiple domains.
90       * @param etcResolvConf <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
91       * @param etcResolverFiles List of files of the format defined in
92       * <a href="
93       * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
94       * /etc/resolver</a>.
95       * @throws IOException If an error occurs while parsing the input files.
96       */
97      public UnixResolverDnsServerAddressStreamProvider(File etcResolvConf, File... etcResolverFiles) throws IOException {
98          Map<String, DnsServerAddresses> etcResolvConfMap = parse(checkNotNull(etcResolvConf, "etcResolvConf"));
99          final boolean useEtcResolverFiles = etcResolverFiles != null && etcResolverFiles.length != 0;
100         domainToNameServerStreamMap = useEtcResolverFiles ? parse(etcResolverFiles) : etcResolvConfMap;
101 
102         DnsServerAddresses defaultNameServerAddresses
103                 = etcResolvConfMap.get(etcResolvConf.getName());
104         if (defaultNameServerAddresses == null) {
105             Collection<DnsServerAddresses> values = etcResolvConfMap.values();
106             if (values.isEmpty()) {
107                 throw new IllegalArgumentException(etcResolvConf + " didn't provide any name servers");
108             }
109             this.defaultNameServerAddresses = values.iterator().next();
110         } else {
111             this.defaultNameServerAddresses = defaultNameServerAddresses;
112         }
113 
114         if (useEtcResolverFiles) {
115             domainToNameServerStreamMap.putAll(etcResolvConfMap);
116         }
117     }
118 
119     /**
120      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> which may contain
121      * the default DNS server to use, and also overrides for individual domains. Also parse a directory of the format
122      * <a href="
123      * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
124      * /etc/resolver</a> which may contain multiple files to override the name servers used for multiple domains.
125      * @param etcResolvConf <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
126      * @param etcResolverDir Directory containing files of the format defined in
127      * <a href="
128      * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
129      * /etc/resolver</a>.
130      * @throws IOException If an error occurs while parsing the input files.
131      */
132     public UnixResolverDnsServerAddressStreamProvider(String etcResolvConf, String etcResolverDir) throws IOException {
133         this(etcResolvConf == null ? null : new File(etcResolvConf),
134              etcResolverDir == null ? null : new File(etcResolverDir).listFiles());
135     }
136 
137     @Override
138     public DnsServerAddressStream nameServerAddressStream(String hostname) {
139         for (;;) {
140             int i = hostname.indexOf('.', 1);
141             if (i < 0 || i == hostname.length() - 1) {
142                 return defaultNameServerAddresses.stream();
143             }
144 
145             DnsServerAddresses addresses = domainToNameServerStreamMap.get(hostname);
146             if (addresses != null) {
147                 return addresses.stream();
148             }
149 
150             hostname = hostname.substring(i + 1);
151         }
152     }
153 
154     private boolean mayOverrideNameServers() {
155         return !domainToNameServerStreamMap.isEmpty() || defaultNameServerAddresses.stream().next() != null;
156     }
157 
158     private static Map<String, DnsServerAddresses> parse(File... etcResolverFiles) throws IOException {
159         Map<String, DnsServerAddresses> domainToNameServerStreamMap =
160                 new HashMap<String, DnsServerAddresses>(etcResolverFiles.length << 1);
161         boolean rotateGlobal = RES_OPTIONS != null && RES_OPTIONS.contains(OPTIONS_ROTATE_FLAG);
162         for (File etcResolverFile : etcResolverFiles) {
163             if (!etcResolverFile.isFile()) {
164                 continue;
165             }
166             FileReader fr = new FileReader(etcResolverFile);
167             BufferedReader br = null;
168             try {
169                 br = new BufferedReader(fr);
170                 List<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>(2);
171                 String domainName = etcResolverFile.getName();
172                 boolean rotate = rotateGlobal;
173                 int port = DNS_PORT;
174                 String line;
175                 while ((line = br.readLine()) != null) {
176                     line = line.trim();
177                     try {
178                         char c;
179                         if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
180                             continue;
181                         }
182                         if (!rotate && line.startsWith(OPTIONS_ROW_LABEL)) {
183                             rotate = line.contains(OPTIONS_ROTATE_FLAG);
184                         } else if (line.startsWith(NAMESERVER_ROW_LABEL)) {
185                             int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
186                             if (i < 0) {
187                                 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
188                                         " in file " + etcResolverFile + ". value: " + line);
189                             }
190                             String maybeIP;
191                             int x = indexOfWhiteSpace(line, i);
192                             if (x == -1) {
193                                 maybeIP = line.substring(i);
194                             } else {
195                                 // ignore comments
196                                 int idx = indexOfNonWhiteSpace(line, x);
197                                 if (idx == -1 || line.charAt(idx) != '#') {
198                                     throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
199                                             " in file " + etcResolverFile + ". value: " + line);
200                                 }
201                                 maybeIP = line.substring(i, x);
202                             }
203 
204                             // There may be a port appended onto the IP address so we attempt to extract it.
205                             if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
206                                 i = maybeIP.lastIndexOf('.');
207                                 if (i + 1 >= maybeIP.length()) {
208                                     throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
209                                             " in file " + etcResolverFile + ". invalid IP value: " + line);
210                                 }
211                                 port = Integer.parseInt(maybeIP.substring(i + 1));
212                                 maybeIP = maybeIP.substring(0, i);
213                             }
214                             InetSocketAddress addr = SocketUtils.socketAddress(maybeIP, port);
215                             // Check if the address is resolved and only if this is the case use it. Otherwise just
216                             // ignore it. This is needed to filter out invalid entries, as if for example an ipv6
217                             // address is used with a scope that represent a network interface that does not exists
218                             // on the host.
219                             if (!addr.isUnresolved()) {
220                                 addresses.add(addr);
221                             }
222                         } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
223                             int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
224                             if (i < 0) {
225                                 throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
226                                         " in file " + etcResolverFile + " value: " + line);
227                             }
228                             domainName = line.substring(i);
229                             if (!addresses.isEmpty()) {
230                                 putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
231                             }
232                             addresses = new ArrayList<InetSocketAddress>(2);
233                         } else if (line.startsWith(PORT_ROW_LABEL)) {
234                             int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
235                             if (i < 0) {
236                                 throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
237                                         " in file " + etcResolverFile + " value: " + line);
238                             }
239                             port = Integer.parseInt(line.substring(i));
240                         } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
241                             logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line);
242                         }
243                     } catch (IllegalArgumentException e) {
244                         logger.warn("Could not parse entry. Ignoring line: {}", line, e);
245                     }
246                 }
247                 if (!addresses.isEmpty()) {
248                     putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
249                 }
250             } finally {
251                 if (br == null) {
252                     fr.close();
253                 } else {
254                     br.close();
255                 }
256             }
257         }
258         return domainToNameServerStreamMap;
259     }
260 
261     private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
262                                     String domainName,
263                                     List<InetSocketAddress> addresses,
264                                     boolean rotate) {
265         // TODO(scott): sortlist is being ignored.
266         DnsServerAddresses addrs = rotate
267             ? DnsServerAddresses.rotational(addresses)
268             : DnsServerAddresses.sequential(addresses);
269         putIfAbsent(domainToNameServerStreamMap, domainName, addrs);
270     }
271 
272     private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
273                                     String domainName,
274                                     DnsServerAddresses addresses) {
275         DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses);
276         if (existingAddresses != null) {
277             domainToNameServerStreamMap.put(domainName, existingAddresses);
278             if (logger.isDebugEnabled()) {
279                 logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
280                         domainName, existingAddresses, addresses);
281             }
282         }
283     }
284 
285     /**
286      * Parse <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return options of interest, namely:
287      * timeout, attempts and ndots.
288      * @return The options values provided by /etc/resolve.conf.
289      * @throws IOException If a failure occurs parsing the file.
290      */
291     static UnixResolverOptions parseEtcResolverOptions() throws IOException {
292         return parseEtcResolverOptions(new File(ETC_RESOLV_CONF_FILE));
293     }
294 
295     /**
296      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return options
297      * of interest, namely: timeout, attempts and ndots.
298      * @param etcResolvConf a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
299      * @return The options values provided by /etc/resolve.conf.
300      * @throws IOException If a failure occurs parsing the file.
301      */
302     static UnixResolverOptions parseEtcResolverOptions(File etcResolvConf) throws IOException {
303         UnixResolverOptions.Builder optionsBuilder = UnixResolverOptions.newBuilder();
304 
305         FileReader fr = new FileReader(etcResolvConf);
306         BufferedReader br = null;
307         try {
308             br = new BufferedReader(fr);
309             String line;
310             while ((line = br.readLine()) != null) {
311                 if (line.startsWith(OPTIONS_ROW_LABEL)) {
312                     parseResOptions(line.substring(OPTIONS_ROW_LABEL.length()), optionsBuilder);
313                     break;
314                 }
315             }
316         } finally {
317             if (br == null) {
318                 fr.close();
319             } else {
320                 br.close();
321             }
322         }
323 
324         // amend options
325         if (RES_OPTIONS != null) {
326             parseResOptions(RES_OPTIONS, optionsBuilder);
327         }
328 
329         return optionsBuilder.build();
330     }
331 
332     private static void parseResOptions(String line, UnixResolverOptions.Builder builder) {
333         String[] opts = WHITESPACE_PATTERN.split(line);
334         for (String opt : opts) {
335             try {
336                 if (opt.startsWith("ndots:")) {
337                     builder.setNdots(parseResIntOption(opt, "ndots:"));
338                 } else if (opt.startsWith("attempts:")) {
339                     builder.setAttempts(parseResIntOption(opt, "attempts:"));
340                 } else if (opt.startsWith("timeout:")) {
341                     builder.setTimeout(parseResIntOption(opt, "timeout:"));
342                 }
343             } catch (NumberFormatException ignore) {
344                 // skip bad int values from resolv.conf to keep value already set in UnixResolverOptions
345             }
346         }
347     }
348 
349     private static int parseResIntOption(String opt, String fullLabel) {
350         String optValue = opt.substring(fullLabel.length());
351         return Integer.parseInt(optValue);
352     }
353 
354     /**
355      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
356      * list of search domains found in it or an empty list if not found.
357      * @return List of search domains.
358      * @throws IOException If a failure occurs parsing the file.
359      */
360     static List<String> parseEtcResolverSearchDomains() throws IOException {
361         return parseEtcResolverSearchDomains(new File(ETC_RESOLV_CONF_FILE));
362     }
363 
364     /**
365      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
366      * list of search domains found in it or an empty list if not found.
367      * @param etcResolvConf a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
368      * @return List of search domains.
369      * @throws IOException If a failure occurs parsing the file.
370      */
371     static List<String> parseEtcResolverSearchDomains(File etcResolvConf) throws IOException {
372         String localDomain = null;
373         List<String> searchDomains = new ArrayList<String>();
374 
375         FileReader fr = new FileReader(etcResolvConf);
376         BufferedReader br = null;
377         try {
378             br = new BufferedReader(fr);
379             String line;
380             while ((line = br.readLine()) != null) {
381                 if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) {
382                     int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
383                     if (i >= 0) {
384                         localDomain = line.substring(i);
385                     }
386                 } else if (line.startsWith(SEARCH_ROW_LABEL)) {
387                     int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length());
388                     if (i >= 0) {
389                         // May contain more then one entry, either separated by whitespace or tab.
390                         // See https://linux.die.net/man/5/resolver
391                         String[] domains = WHITESPACE_PATTERN.split(line.substring(i));
392                         Collections.addAll(searchDomains, domains);
393                     }
394                 }
395             }
396         } finally {
397             if (br == null) {
398                 fr.close();
399             } else {
400                 br.close();
401             }
402         }
403 
404         // return what was on the 'domain' line only if there were no 'search' lines
405         return localDomain != null && searchDomains.isEmpty()
406                 ? Collections.singletonList(localDomain)
407                 : searchDomains;
408     }
409 
410 }