001/*
002 * Copyright 2015-2024 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        final Level TRACE_LOG_LEVEL = Level.FINE;
252        for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) {
253            try {
254                res = mechanism.getDnsServerAddresses();
255            } catch (SecurityException exception) {
256                LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception);
257            }
258            if (res == null) {
259                LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server");
260                continue;
261            }
262
263            if (LOGGER.isLoggable(TRACE_LOG_LEVEL)) {
264                // TODO: Use String.join() once MiniDNS is Android API 26 (or higher).
265                StringBuilder sb = new StringBuilder();
266                for (Iterator<String> it = res.iterator(); it.hasNext();) {
267                    String s = it.next();
268                    sb.append(s);
269                    if (it.hasNext()) {
270                        sb.append(", ");
271                    }
272                }
273                String dnsServers = sb.toString();
274                LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism {0} returned the following DNS servers: {1}",
275                        new Object[] { mechanism.getName(), dnsServers });
276            }
277
278            assert !res.isEmpty();
279
280            // We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial
281            // though, because the list returned by the server mechanism is rather short.
282
283            // Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else,
284            // especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using
285            // getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside
286            // of its DNSSEC guarantees.
287            Iterator<String> it = res.iterator();
288            while (it.hasNext()) {
289                String potentialDnsServer = it.next();
290                if (!InetAddressUtil.isIpAddress(potentialDnsServer)) {
291                    LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
292                            + "' returned an invalid non-IP address result: '" + potentialDnsServer + "'");
293                    it.remove();
294                } else if (blacklistedDnsServers.contains(potentialDnsServer)) {
295                    LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName()
296                    + "' returned a blacklisted result: '" + potentialDnsServer + "'");
297                    it.remove();
298                }
299            }
300
301            if (!res.isEmpty()) {
302                break;
303            }
304
305            LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
306                        + "' returned not a single valid IP address after sanitazion");
307            res = null;
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        int validServerAddresses = 0;
344        for (String dnsServerString : res) {
345            // The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings
346            // representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being
347            // involved. Something we want to avoid.
348            assert InetAddressUtil.isIpAddress(dnsServerString);
349
350            InetAddress dnsServerAddress;
351            try {
352                dnsServerAddress = InetAddress.getByName(dnsServerString);
353            } catch (UnknownHostException e) {
354                LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e);
355                continue;
356            }
357            if (dnsServerAddress instanceof Inet4Address) {
358                if (!setting.v4) {
359                    continue;
360                }
361                Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress;
362                ipv4DnsServer.add(ipv4DnsServerAddress);
363            } else if (dnsServerAddress instanceof Inet6Address) {
364                if (!setting.v6) {
365                    continue;
366                }
367                Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress;
368                ipv6DnsServer.add(ipv6DnsServerAddress);
369            } else {
370                throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address");
371            }
372
373            validServerAddresses++;
374        }
375
376        List<InetAddress> dnsServers = new ArrayList<>(validServerAddresses);
377
378        switch (setting) {
379        case v4v6:
380            dnsServers.addAll(ipv4DnsServer);
381            dnsServers.addAll(ipv6DnsServer);
382            break;
383        case v6v4:
384            dnsServers.addAll(ipv6DnsServer);
385            dnsServers.addAll(ipv4DnsServer);
386            break;
387        case v4only:
388            dnsServers.addAll(ipv4DnsServer);
389            break;
390        case v6only:
391            dnsServers.addAll(ipv6DnsServer);
392            break;
393        }
394        return dnsServers;
395    }
396
397    public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
398        if (!dnsServerLookup.isAvailable()) {
399            LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available.");
400            return;
401        }
402        synchronized (LOOKUP_MECHANISMS) {
403            // We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace
404            // LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see
405            // http://stackoverflow.com/a/34827492/194894
406            // TODO: Remove that workaround once MiniDNS is Java 8 only.
407            ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1);
408            tempList.addAll(LOOKUP_MECHANISMS);
409            tempList.add(dnsServerLookup);
410
411            // Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7.
412            Collections.sort(tempList);
413
414            LOOKUP_MECHANISMS.clear();
415            LOOKUP_MECHANISMS.addAll(tempList);
416        }
417    }
418
419    public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
420        synchronized (LOOKUP_MECHANISMS) {
421            return LOOKUP_MECHANISMS.remove(dnsServerLookup);
422        }
423    }
424
425    public static boolean addBlacklistedDnsServer(String dnsServer) {
426        return blacklistedDnsServers.add(dnsServer);
427    }
428
429    public static boolean removeBlacklistedDnsServer(String dnsServer) {
430        return blacklistedDnsServers.remove(dnsServer);
431    }
432
433    public boolean isAskForDnssec() {
434        return askForDnssec;
435    }
436
437    public void setAskForDnssec(boolean askForDnssec) {
438        this.askForDnssec = askForDnssec;
439    }
440
441    public boolean isDisableResultFilter() {
442        return disableResultFilter;
443    }
444
445    public void setDisableResultFilter(boolean disableResultFilter) {
446        this.disableResultFilter = disableResultFilter;
447    }
448
449    public boolean isUseHardcodedDnsServersEnabled() {
450        return useHardcodedDnsServers;
451    }
452
453    public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) {
454        this.useHardcodedDnsServers = useHardcodedDnsServers;
455    }
456
457    public InetAddress getRandomHardcodedIpv4DnsServer() {
458        return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom);
459    }
460
461    public InetAddress getRandomHarcodedIpv6DnsServer() {
462        return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom);
463    }
464
465    private static Question getReverseIpLookupQuestionFor(DnsName dnsName) {
466        return new Question(dnsName, TYPE.PTR);
467    }
468
469    public static Question getReverseIpLookupQuestionFor(Inet4Address inet4Address) {
470        DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet4Address);
471        DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IN_ADDR_ARPA);
472        return getReverseIpLookupQuestionFor(dnsName);
473    }
474
475    public static Question getReverseIpLookupQuestionFor(Inet6Address inet6Address) {
476        DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet6Address);
477        DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IP6_ARPA);
478        return getReverseIpLookupQuestionFor(dnsName);
479    }
480
481    public static Question getReverseIpLookupQuestionFor(InetAddress inetAddress) {
482        if (inetAddress instanceof Inet4Address) {
483            return getReverseIpLookupQuestionFor((Inet4Address) inetAddress);
484        } else if (inetAddress instanceof Inet6Address) {
485            return getReverseIpLookupQuestionFor((Inet6Address) inetAddress);
486        } else {
487            throw new IllegalArgumentException("The provided inetAddress '" + inetAddress
488                    + "' is neither of type Inet4Address nor Inet6Address");
489        }
490     }
491
492}