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