001/*
002 * Copyright 2015-2018 the original author or authors
003 *
004 * This software is licensed under the Apache License, Version 2.0,
005 * the GNU Lesser General Public License version 2 or later ("LGPL")
006 * and the WTFPL.
007 * You may choose either license to govern your use of this software only
008 * upon the condition that you accept all of the terms of either
009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
010 */
011package org.minidns;
012
013import org.minidns.MiniDnsFuture.InternalMiniDnsFuture;
014import org.minidns.dnsmessage.DnsMessage;
015import org.minidns.dnsserverlookup.AndroidUsingExec;
016import org.minidns.dnsserverlookup.AndroidUsingReflection;
017import org.minidns.dnsserverlookup.DnsServerLookupMechanism;
018import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf;
019import org.minidns.util.CollectionsUtil;
020import org.minidns.util.ExceptionCallback;
021import org.minidns.util.InetAddressUtil;
022import org.minidns.util.SuccessCallback;
023import org.minidns.util.MultipleIoException;
024
025import java.io.IOException;
026import java.net.Inet4Address;
027import java.net.Inet6Address;
028import java.net.InetAddress;
029import java.net.UnknownHostException;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Iterator;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Set;
036import java.util.concurrent.ConcurrentHashMap;
037import java.util.concurrent.CopyOnWriteArrayList;
038import java.util.concurrent.CopyOnWriteArraySet;
039import java.util.logging.Level;
040
041/**
042 * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
043 * This circumvents the missing javax.naming package on android.
044 */
045public class DnsClient extends AbstractDnsClient {
046
047    static final List<DnsServerLookupMechanism> LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>();
048
049    static final Set<Inet4Address> STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>();
050    static final Set<Inet6Address> STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>();
051
052    static {
053        addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE);
054        addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE);
055        addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE);
056
057        try {
058            Inet4Address googleV4Dns = InetAddressUtil.ipv4From("8.8.8.8");
059            STATIC_IPV4_DNS_SERVERS.add(googleV4Dns);
060        } catch (IllegalArgumentException e) {
061            LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e);
062        }
063
064        try {
065            Inet6Address googleV6Dns = InetAddressUtil.ipv6From("[2001:4860:4860::8888]");
066            STATIC_IPV6_DNS_SERVERS.add(googleV6Dns);
067        } catch (IllegalArgumentException e) {
068            LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e);
069        }
070    }
071
072    private static final Set<String> blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(4));
073
074    private final Set<InetAddress> nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>(4));
075
076    private boolean askForDnssec = false;
077    private boolean disableResultFilter = false;
078
079    private boolean useHardcodedDnsServers = true;
080
081    /**
082     * Create a new DNS client using the global default cache.
083     */
084    public DnsClient() {
085        super();
086    }
087
088    public DnsClient(DnsCache dnsCache) {
089        super(dnsCache);
090    }
091
092    @Override
093    protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
094        message.setRecursionDesired(true);
095        message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(askForDnssec);
096        return message;
097    }
098
099    private List<InetAddress> getServerAddresses() {
100        List<InetAddress> dnsServerAddresses = findDnsAddresses();
101
102        InetAddress[] selectedHardcodedDnsServerAddresses = new InetAddress[2];
103        if (useHardcodedDnsServers) {
104            InetAddress primaryHardcodedDnsServer = null, secondaryHardcodedDnsServer = null;
105            switch (ipVersionSetting) {
106            case v4v6:
107                primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
108                secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
109                break;
110            case v6v4:
111                primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
112                secondaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
113                break;
114            case v4only:
115                primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
116                break;
117            case v6only:
118                primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
119                break;
120            }
121            selectedHardcodedDnsServerAddresses[0] = primaryHardcodedDnsServer;
122            selectedHardcodedDnsServerAddresses[1] = secondaryHardcodedDnsServer;
123        }
124        for (InetAddress selectedHardcodedDnsServerAddress : selectedHardcodedDnsServerAddresses) {
125            if (selectedHardcodedDnsServerAddress == null) continue;
126            dnsServerAddresses.add(selectedHardcodedDnsServerAddress);
127        }
128
129        return dnsServerAddresses;
130    }
131
132    @Override
133    public DnsMessage query(DnsMessage.Builder queryBuilder) throws IOException {
134        DnsMessage q = newQuestion(queryBuilder).build();
135        // While this query method does in fact re-use query(Question, String)
136        // we still do a cache lookup here in order to avoid unnecessary
137        // findDNS()calls, which are expensive on Android. Note that we do not
138        // put the results back into the Cache, as this is already done by
139        // query(Question, String).
140        DnsMessage responseMessage = (cache == null) ? null : cache.get(q);
141        if (responseMessage != null) {
142            return responseMessage;
143        }
144
145        List<InetAddress> dnsServerAddresses = getServerAddresses();
146
147        List<IOException> ioExceptions = new ArrayList<>(dnsServerAddresses.size());
148        for (InetAddress dns : dnsServerAddresses) {
149            if (nonRaServers.contains(dns)) {
150                LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\"");
151                continue;
152            }
153
154            try {
155                responseMessage = query(q, dns);
156                if (responseMessage == null) {
157                    continue;
158                }
159
160                if (!responseMessage.recursionAvailable) {
161                    boolean newRaServer = nonRaServers.add(dns);
162                    if (newRaServer) {
163                        LOGGER.warning("The DNS server "
164                                + dns
165                                + " returned a response without the \"recursion available\" (RA) flag set. This likely indicates a misconfiguration because the server is not suitable for DNS resolution");
166                    }
167                    continue;
168                }
169
170                if (disableResultFilter) {
171                    return responseMessage;
172                }
173
174                switch (responseMessage.responseCode) {
175                case NO_ERROR:
176                case NX_DOMAIN:
177                    break;
178                default:
179                    String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: "
180                            + responseMessage.responseCode + '.';
181                    if (!LOGGER.isLoggable(Level.FINE)) {
182                        // Only append the responseMessage is log level is not fine. If it is fine or higher, the
183                        // response has already been logged.
184                        warning += "\n" + responseMessage;
185                    }
186                    LOGGER.warning(warning);
187                    // TODO Create new IOException and add to ioExceptions.
188                    continue;
189                }
190
191                return responseMessage;
192            } catch (IOException ioe) {
193                ioExceptions.add(ioe);
194            }
195        }
196        MultipleIoException.throwIfRequired(ioExceptions);
197        // TODO assert that we never return null here.
198        return null;
199    }
200
201    @Override
202    protected MiniDnsFuture<DnsMessage, IOException> queryAsync(DnsMessage.Builder queryBuilder) {
203        DnsMessage q = newQuestion(queryBuilder).build();
204        // While this query method does in fact re-use query(Question, String)
205        // we still do a cache lookup here in order to avoid unnecessary
206        // findDNS()calls, which are expensive on Android. Note that we do not
207        // put the results back into the Cache, as this is already done by
208        // query(Question, String).
209        DnsMessage responseMessage = (cache == null) ? null : cache.get(q);
210        if (responseMessage != null) {
211            return MiniDnsFuture.from(responseMessage);
212        }
213
214        final List<InetAddress> dnsServerAddresses = getServerAddresses();
215
216        final InternalMiniDnsFuture<DnsMessage, IOException> future = new InternalMiniDnsFuture<>();
217        final List<IOException> exceptions = Collections.synchronizedList(new ArrayList<IOException>(dnsServerAddresses.size()));
218
219        // Filter loop.
220        Iterator<InetAddress> it = dnsServerAddresses.iterator();
221        while (it.hasNext()) {
222            InetAddress dns = it.next();
223            if (nonRaServers.contains(dns)) {
224                it.remove();
225                LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\"");
226                continue;
227            }
228        }
229
230        List<MiniDnsFuture<DnsMessage, IOException>> futures = new ArrayList<>(dnsServerAddresses.size());
231        // "Main" loop.
232        for (InetAddress dns : dnsServerAddresses) {
233            if (future.isDone()) {
234                for (MiniDnsFuture<DnsMessage, IOException> futureToCancel : futures) {
235                    futureToCancel.cancel(true);
236                }
237                break;
238            }
239
240            MiniDnsFuture<DnsMessage, IOException> f = queryAsync(q, dns);
241            f.onSuccess(new SuccessCallback<DnsMessage>() {
242                @Override
243                public void onSuccess(DnsMessage result) {
244                    future.setResult(result);
245                }
246            });
247            f.onError(new ExceptionCallback<IOException>() {
248                @Override
249                public void processException(IOException exception) {
250                    exceptions.add(exception);
251                    if (exceptions.size() == dnsServerAddresses.size()) {
252                        future.setException(MultipleIoException.toIOException(exceptions));
253                    }
254                }
255            });
256            futures.add(f);
257        }
258
259        return future;
260    }
261
262    /**
263     * Retrieve a list of currently configured DNS servers IP addresses. This method does verify that only IP addresses are returned and
264     * nothing else (e.g. DNS names).
265     * <p>
266     * The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s.
267     * </p>
268     *
269     * @return A list of DNS server IP addresses configured for this system.
270     */
271    public static List<String> findDNS() {
272        List<String> res = null;
273        for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) {
274            res = mechanism.getDnsServerAddresses();
275            if (res == null) {
276                continue;
277            }
278
279            assert(!res.isEmpty());
280
281            // We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial
282            // though, because the list returned by the server mechanism is rather short.
283
284            // Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else,
285            // especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using
286            // getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside
287            // of its DNSSEC guarantees.
288            Iterator<String> it = res.iterator();
289            while (it.hasNext()) {
290                String potentialDnsServer = it.next();
291                if (!InetAddressUtil.isIpAddress(potentialDnsServer)) {
292                    LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
293                            + "' returned an invalid non-IP address result: '" + potentialDnsServer + "'");
294                    it.remove();
295                } else if (blacklistedDnsServers.contains(potentialDnsServer)) {
296                    LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName()
297                    + "' returned a blacklisted result: '" + potentialDnsServer + "'");
298                    it.remove();
299                }
300            }
301
302            if (!res.isEmpty()) {
303                break;
304            } else {
305                LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
306                        + "' returned not a single valid IP address after sanitazion");
307            }
308        }
309
310        return res;
311    }
312
313    /**
314     * Retrieve a list of currently configured DNS server addresses.
315     * <p>
316     * Note that unlike {@link #findDNS()}, the list returned by this method
317     * will take the IP version setting into account, and order the list by the
318     * preferred address types (IPv4/v6). The returned list is modifiable.
319     * </p>
320     *
321     * @return A list of DNS server addresses.
322     * @see #findDNS()
323     */
324    public static List<InetAddress> findDnsAddresses() {
325        // The findDNS() method contract guarantees that only IP addresses will be returned.
326        List<String> res = findDNS();
327
328        if (res == null) {
329            return new ArrayList<>();
330        }
331
332        final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING;
333
334        List<Inet4Address> ipv4DnsServer = null;
335        List<Inet6Address> ipv6DnsServer = null;
336        if (setting.v4) {
337            ipv4DnsServer = new ArrayList<>(res.size());
338        }
339        if (setting.v6) {
340            ipv6DnsServer = new ArrayList<>(res.size());
341        }
342
343        for (String dnsServerString : res) {
344            // The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings
345            // representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being
346            // involved. Something we want to avoid.
347            assert (InetAddressUtil.isIpAddress(dnsServerString));
348
349            InetAddress dnsServerAddress;
350            try {
351                dnsServerAddress = InetAddress.getByName(dnsServerString);
352            } catch (UnknownHostException e) {
353                LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e);
354                continue;
355            }
356            if (dnsServerAddress instanceof Inet4Address) {
357                if (!setting.v4) {
358                    continue;
359                }
360                Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress;
361                ipv4DnsServer.add(ipv4DnsServerAddress);
362            } else if (dnsServerAddress instanceof Inet6Address) {
363                if (!setting.v6) {
364                    continue;
365                }
366                Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress;
367                ipv6DnsServer.add(ipv6DnsServerAddress);
368            } else {
369                throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address");
370            }
371        }
372
373        List<InetAddress> dnsServers = new LinkedList<>();
374
375        switch (setting) {
376        case v4v6:
377            dnsServers.addAll(ipv4DnsServer);
378            dnsServers.addAll(ipv6DnsServer);
379            break;
380        case v6v4:
381            dnsServers.addAll(ipv6DnsServer);
382            dnsServers.addAll(ipv4DnsServer);
383            break;
384        case v4only:
385            dnsServers.addAll(ipv4DnsServer);
386            break;
387        case v6only:
388            dnsServers.addAll(ipv6DnsServer);
389            break;
390        }
391        return dnsServers;
392    }
393
394    public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
395        if (!dnsServerLookup.isAvailable()) {
396            LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available.");
397            return;
398        }
399        synchronized (LOOKUP_MECHANISMS) {
400            // We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace
401            // LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see
402            // http://stackoverflow.com/a/34827492/194894
403            // TODO: Remove that workaround once MiniDNS is Java 8 only.
404            ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1);
405            tempList.addAll(LOOKUP_MECHANISMS);
406            tempList.add(dnsServerLookup);
407
408            // Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7.
409            Collections.sort(tempList);
410
411            LOOKUP_MECHANISMS.clear();
412            LOOKUP_MECHANISMS.addAll(tempList);
413        }
414    }
415
416    public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
417        synchronized (LOOKUP_MECHANISMS) {
418            return LOOKUP_MECHANISMS.remove(dnsServerLookup);
419        }
420    }
421
422    public static boolean addBlacklistedDnsServer(String dnsServer) {
423        return blacklistedDnsServers.add(dnsServer);
424    }
425
426    public static boolean removeBlacklistedDnsServer(String dnsServer) {
427        return blacklistedDnsServers.remove(dnsServer);
428    }
429
430    public boolean isAskForDnssec() {
431        return askForDnssec;
432    }
433
434    public void setAskForDnssec(boolean askForDnssec) {
435        this.askForDnssec = askForDnssec;
436    }
437
438    public boolean isDisableResultFilter() {
439        return disableResultFilter;
440    }
441
442    public void setDisableResultFilter(boolean disableResultFilter) {
443        this.disableResultFilter = disableResultFilter;
444    }
445
446    public boolean isUseHardcodedDnsServersEnabled() {
447        return useHardcodedDnsServers;
448    }
449
450    public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) {
451        this.useHardcodedDnsServers = useHardcodedDnsServers;
452    }
453
454    public InetAddress getRandomHardcodedIpv4DnsServer() {
455        return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom);
456    }
457
458    public InetAddress getRandomHarcodedIpv6DnsServer() {
459        return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom);
460    }
461}