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