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