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    *   http://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.UnstableApi;
21  import io.netty.util.internal.logging.InternalLogger;
22  import io.netty.util.internal.logging.InternalLoggerFactory;
23  
24  import java.io.BufferedReader;
25  import java.io.File;
26  import java.io.FileReader;
27  import java.io.IOException;
28  import java.net.InetSocketAddress;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.regex.Pattern;
36  
37  import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
38  import static io.netty.util.internal.ObjectUtil.checkNotNull;
39  import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace;
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  @UnstableApi
47  public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
48      private static final InternalLogger logger =
49              InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class);
50      private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf";
51      private static final String ETC_RESOLVER_DIR = "/etc/resolver";
52      private static final String NAMESERVER_ROW_LABEL = "nameserver";
53      private static final String SORTLIST_ROW_LABEL = "sortlist";
54      private static final String OPTIONS_ROW_LABEL = "options";
55      private static final String DOMAIN_ROW_LABEL = "domain";
56      private static final String SEARCH_ROW_LABEL = "search";
57      private static final String PORT_ROW_LABEL = "port";
58      private static final String NDOTS_LABEL = "ndots:";
59      static final int DEFAULT_NDOTS = 1;
60      private final DnsServerAddresses defaultNameServerAddresses;
61      private final Map<String, DnsServerAddresses> domainToNameServerStreamMap;
62      private static final Pattern SEARCH_DOMAIN_PATTERN = Pattern.compile("\\s+");
63  
64      /**
65       * Attempt to parse {@code /etc/resolv.conf} and files in the {@code /etc/resolver} directory by default.
66       * A failure to parse will return {@link DefaultDnsServerAddressStreamProvider}.
67       */
68      static DnsServerAddressStreamProvider parseSilently() {
69          try {
70              UnixResolverDnsServerAddressStreamProvider nameServerCache =
71                      new UnixResolverDnsServerAddressStreamProvider(ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR);
72              return nameServerCache.mayOverrideNameServers() ? nameServerCache
73                                                              : DefaultDnsServerAddressStreamProvider.INSTANCE;
74          } catch (Exception e) {
75              logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e);
76              return DefaultDnsServerAddressStreamProvider.INSTANCE;
77          }
78      }
79  
80      /**
81       * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> which may contain
82       * the default DNS server to use, and also overrides for individual domains. Also parse list of files of the format
83       * <a href="
84       * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
85       * /etc/resolver</a> which may contain multiple files to override the name servers used for multimple domains.
86       * @param etcResolvConf <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
87       * @param etcResolverFiles List of files of the format defined in
88       * <a href="
89       * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
90       * /etc/resolver</a>.
91       * @throws IOException If an error occurs while parsing the input files.
92       */
93      public UnixResolverDnsServerAddressStreamProvider(File etcResolvConf, File... etcResolverFiles) throws IOException {
94          Map<String, DnsServerAddresses> etcResolvConfMap = parse(checkNotNull(etcResolvConf, "etcResolvConf"));
95          final boolean useEtcResolverFiles = etcResolverFiles != null && etcResolverFiles.length != 0;
96          domainToNameServerStreamMap = useEtcResolverFiles ? parse(etcResolverFiles) : etcResolvConfMap;
97  
98          DnsServerAddresses defaultNameServerAddresses = etcResolvConfMap.get(etcResolvConf.getName());
99          if (defaultNameServerAddresses == null) {
100             Collection<DnsServerAddresses> values = etcResolvConfMap.values();
101             if (values.isEmpty()) {
102                 throw new IllegalArgumentException(etcResolvConf + " didn't provide any name servers");
103             }
104             this.defaultNameServerAddresses = values.iterator().next();
105         } else {
106             this.defaultNameServerAddresses = defaultNameServerAddresses;
107         }
108 
109         if (useEtcResolverFiles) {
110             domainToNameServerStreamMap.putAll(etcResolvConfMap);
111         }
112     }
113 
114     /**
115      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> which may contain
116      * the default DNS server to use, and also overrides for individual domains. Also parse a directory of the format
117      * <a href="
118      * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
119      * /etc/resolver</a> which may contain multiple files to override the name servers used for multimple domains.
120      * @param etcResolvConf <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
121      * @param etcResolverDir Directory containing files of the format defined in
122      * <a href="
123      * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
124      * /etc/resolver</a>.
125      * @throws IOException If an error occurs while parsing the input files.
126      */
127     public UnixResolverDnsServerAddressStreamProvider(String etcResolvConf, String etcResolverDir) throws IOException {
128         this(etcResolvConf == null ? null : new File(etcResolvConf),
129              etcResolverDir == null ? null : new File(etcResolverDir).listFiles());
130     }
131 
132     @Override
133     public DnsServerAddressStream nameServerAddressStream(String hostname) {
134         for (;;) {
135             int i = hostname.indexOf('.', 1);
136             if (i < 0 || i == hostname.length() - 1) {
137                 return defaultNameServerAddresses.stream();
138             }
139 
140             DnsServerAddresses addresses = domainToNameServerStreamMap.get(hostname);
141             if (addresses != null) {
142                 return addresses.stream();
143             }
144 
145             hostname = hostname.substring(i + 1);
146         }
147     }
148 
149     private boolean mayOverrideNameServers() {
150         return !domainToNameServerStreamMap.isEmpty() || defaultNameServerAddresses.stream().next() != null;
151     }
152 
153     private static Map<String, DnsServerAddresses> parse(File... etcResolverFiles) throws IOException {
154         Map<String, DnsServerAddresses> domainToNameServerStreamMap =
155                 new HashMap<String, DnsServerAddresses>(etcResolverFiles.length << 1);
156         for (File etcResolverFile : etcResolverFiles) {
157             if (!etcResolverFile.isFile()) {
158                 continue;
159             }
160             FileReader fr = new FileReader(etcResolverFile);
161             BufferedReader br = null;
162             try {
163                 br = new BufferedReader(fr);
164                 List<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>(2);
165                 String domainName = etcResolverFile.getName();
166                 int port = DNS_PORT;
167                 String line;
168                 while ((line = br.readLine()) != null) {
169                     line = line.trim();
170                     char c;
171                     if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
172                         continue;
173                     }
174                     if (line.startsWith(NAMESERVER_ROW_LABEL)) {
175                         int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
176                         if (i < 0) {
177                             throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
178                                     " in file " + etcResolverFile + ". value: " + line);
179                         }
180                         String maybeIP = line.substring(i);
181                         // There may be a port appended onto the IP address so we attempt to extract it.
182                         if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
183                             i = maybeIP.lastIndexOf('.');
184                             if (i + 1 >= maybeIP.length()) {
185                                 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
186                                         " in file " + etcResolverFile + ". invalid IP value: " + line);
187                             }
188                             port = Integer.parseInt(maybeIP.substring(i + 1));
189                             maybeIP = maybeIP.substring(0, i);
190                         }
191                         addresses.add(SocketUtils.socketAddress(maybeIP, port));
192                     } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
193                         int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
194                         if (i < 0) {
195                             throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
196                                     " in file " + etcResolverFile + " value: " + line);
197                         }
198                         domainName = line.substring(i);
199                         if (!addresses.isEmpty()) {
200                             putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
201                         }
202                         addresses = new ArrayList<InetSocketAddress>(2);
203                     } else if (line.startsWith(PORT_ROW_LABEL)) {
204                         int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
205                         if (i < 0) {
206                             throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
207                                     " in file " + etcResolverFile + " value: " + line);
208                         }
209                         port = Integer.parseInt(line.substring(i));
210                     } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
211                         logger.info("row type {} not supported. ignoring line: {}", SORTLIST_ROW_LABEL, line);
212                     }
213                 }
214                 if (!addresses.isEmpty()) {
215                     putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
216                 }
217             } finally {
218                 if (br == null) {
219                     fr.close();
220                 } else {
221                     br.close();
222                 }
223             }
224         }
225         return domainToNameServerStreamMap;
226     }
227 
228     private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
229                                     String domainName,
230                                     List<InetSocketAddress> addresses) {
231         // TODO(scott): sortlist is being ignored.
232         putIfAbsent(domainToNameServerStreamMap, domainName, DnsServerAddresses.sequential(addresses));
233     }
234 
235     private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
236                                     String domainName,
237                                     DnsServerAddresses addresses) {
238         DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses);
239         if (existingAddresses != null) {
240             domainToNameServerStreamMap.put(domainName, existingAddresses);
241             logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
242                     domainName, existingAddresses, addresses);
243         }
244     }
245 
246     /**
247      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
248      * value corresponding to the first ndots in an options configuration.
249      * @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not
250      * found.
251      * @throws IOException If a failure occurs parsing the file.
252      */
253     static int parseEtcResolverFirstNdots() throws IOException {
254         return parseEtcResolverFirstNdots(new File(ETC_RESOLV_CONF_FILE));
255     }
256 
257     /**
258      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
259      * value corresponding to the first ndots in an options configuration.
260      * @param etcResolvConf a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
261      * @return the value corresponding to the first ndots in an options configuration, or {@link #DEFAULT_NDOTS} if not
262      * found.
263      * @throws IOException If a failure occurs parsing the file.
264      */
265     static int parseEtcResolverFirstNdots(File etcResolvConf) throws IOException {
266         FileReader fr = new FileReader(etcResolvConf);
267         BufferedReader br = null;
268         try {
269             br = new BufferedReader(fr);
270             String line;
271             while ((line = br.readLine()) != null) {
272                 if (line.startsWith(OPTIONS_ROW_LABEL)) {
273                     int i = line.indexOf(NDOTS_LABEL);
274                     if (i >= 0) {
275                         i += NDOTS_LABEL.length();
276                         final int j = line.indexOf(' ', i);
277                         return Integer.parseInt(line.substring(i, j < 0 ? line.length() : j));
278                     }
279                     break;
280                 }
281             }
282         } finally {
283             if (br == null) {
284                 fr.close();
285             } else {
286                 br.close();
287             }
288         }
289         return DEFAULT_NDOTS;
290     }
291 
292     /**
293      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
294      * list of search domains found in it or an empty list if not found.
295      * @return List of search domains.
296      * @throws IOException If a failure occurs parsing the file.
297      */
298     static List<String> parseEtcResolverSearchDomains() throws IOException {
299         return parseEtcResolverSearchDomains(new File(ETC_RESOLV_CONF_FILE));
300     }
301 
302     /**
303      * Parse a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and return the
304      * list of search domains found in it or an empty list if not found.
305      * @param etcResolvConf a file of the format <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a>.
306      * @return List of search domains.
307      * @throws IOException If a failure occurs parsing the file.
308      */
309     static List<String> parseEtcResolverSearchDomains(File etcResolvConf) throws IOException {
310         String localDomain = null;
311         List<String> searchDomains = new ArrayList<String>();
312 
313         FileReader fr = new FileReader(etcResolvConf);
314         BufferedReader br = null;
315         try {
316             br = new BufferedReader(fr);
317             String line;
318             while ((line = br.readLine()) != null) {
319                 if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) {
320                     int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
321                     if (i >= 0) {
322                         localDomain = line.substring(i);
323                     }
324                 } else if (line.startsWith(SEARCH_ROW_LABEL)) {
325                     int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length());
326                     if (i >= 0) {
327                         // May contain more then one entry, either seperated by whitespace or tab.
328                         // See https://linux.die.net/man/5/resolver
329                         String[] domains = SEARCH_DOMAIN_PATTERN.split(line.substring(i));
330                         Collections.addAll(searchDomains, domains);
331                     }
332                 }
333             }
334         } finally {
335             if (br == null) {
336                 fr.close();
337             } else {
338                 br.close();
339             }
340         }
341 
342         // return what was on the 'domain' line only if there were no 'search' lines
343         return localDomain != null && searchDomains.isEmpty()
344                 ? Collections.singletonList(localDomain)
345                 : searchDomains;
346     }
347 }