Resolver.java

  1package eu.siacs.conversations.utils;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.util.Log;
  6
  7import androidx.annotation.NonNull;
  8
  9import com.google.common.base.MoreObjects;
 10import com.google.common.base.Objects;
 11import com.google.common.base.Strings;
 12import com.google.common.base.Throwables;
 13import com.google.common.collect.ImmutableList;
 14import com.google.common.collect.ImmutableMap;
 15import com.google.common.collect.Lists;
 16import com.google.common.collect.Ordering;
 17import com.google.common.net.InetAddresses;
 18import com.google.common.primitives.Ints;
 19import com.google.common.util.concurrent.Futures;
 20import com.google.common.util.concurrent.ListenableFuture;
 21import com.google.common.util.concurrent.MoreExecutors;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.Conversations;
 25import eu.siacs.conversations.xmpp.Jid;
 26
 27import org.minidns.dnsmessage.Question;
 28import org.minidns.dnsname.DnsName;
 29import org.minidns.dnsname.InvalidDnsNameException;
 30import org.minidns.dnsqueryresult.DnsQueryResult;
 31import org.minidns.record.A;
 32import org.minidns.record.AAAA;
 33import org.minidns.record.CNAME;
 34import org.minidns.record.Data;
 35import org.minidns.record.InternetAddressRR;
 36import org.minidns.record.Record;
 37import org.minidns.record.SRV;
 38
 39import java.io.IOException;
 40import java.net.Inet4Address;
 41import java.net.InetAddress;
 42import java.net.UnknownHostException;
 43import java.util.ArrayList;
 44import java.util.Arrays;
 45import java.util.Collection;
 46import java.util.Collections;
 47import java.util.Comparator;
 48import java.util.List;
 49import java.util.Map;
 50import java.util.concurrent.ExecutionException;
 51import java.util.concurrent.ExecutorService;
 52import java.util.concurrent.Executors;
 53
 54import eu.siacs.conversations.Config;
 55import eu.siacs.conversations.R;
 56import eu.siacs.conversations.services.XmppConnectionService;
 57import eu.siacs.conversations.xmpp.Jid;
 58
 59import org.minidns.AbstractDnsClient;
 60import org.minidns.DnsCache;
 61import org.minidns.DnsClient;
 62import org.minidns.cache.LruCache;
 63import org.minidns.dnsmessage.Question;
 64import org.minidns.dnsname.DnsName;
 65import org.minidns.dnssec.DnssecResultNotAuthenticException;
 66import org.minidns.dnssec.DnssecValidationFailedException;
 67import org.minidns.dnsserverlookup.AndroidUsingExec;
 68import org.minidns.hla.DnssecResolverApi;
 69import org.minidns.hla.ResolverApi;
 70import org.minidns.hla.ResolverResult;
 71import org.minidns.iterative.ReliableDnsClient;
 72import org.minidns.record.A;
 73import org.minidns.record.AAAA;
 74import org.minidns.record.CNAME;
 75import org.minidns.record.Data;
 76import org.minidns.record.InternetAddressRR;
 77import org.minidns.record.Record;
 78import org.minidns.record.SRV;
 79
 80public class Resolver {
 81
 82    private static final Comparator<Result> RESULT_COMPARATOR =
 83            (left, right) -> {
 84                if (left.priority == right.priority) {
 85                    if (left.directTls == right.directTls) {
 86                        if (left.ip == null && right.ip == null) {
 87                            return 0;
 88                        } else if (left.ip != null && right.ip != null) {
 89                            if (left.ip instanceof Inet4Address
 90                                    && right.ip instanceof Inet4Address) {
 91                                return 0;
 92                            } else {
 93                                return left.ip instanceof Inet4Address ? -1 : 1;
 94                            }
 95                        } else {
 96                            return left.ip != null ? -1 : 1;
 97                        }
 98                    } else {
 99                        return left.directTls ? -1 : 1;
100                    }
101                } else {
102                    return left.priority - right.priority;
103                }
104            };
105
106    private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12);
107
108    public static final int DEFAULT_PORT_XMPP = 5222;
109
110    private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
111    private static final String STARTTLS_SERVICE = "_xmpp-client";
112
113    private static XmppConnectionService SERVICE = null;
114
115    private static List<String> DNSSECLESS_TLDS = Arrays.asList(
116        "ae",
117        "aero",
118        "ai",
119        "al",
120        "ao",
121        "aq",
122        "as",
123        "ba",
124        "bb",
125        "bd",
126        "bf",
127        "bi",
128        "bj",
129        "bn",
130        "bo",
131        "bs",
132        "bw",
133        "cd",
134        "cf",
135        "cg",
136        "ci",
137        "ck",
138        "cm",
139        "cu",
140        "cv",
141        "cw",
142        "dj",
143        "dm",
144        "do",
145        "ec",
146        "eg",
147        "eh",
148        "er",
149        "et",
150        "fj",
151        "fk",
152        "ga",
153        "ge",
154        "gf",
155        "gh",
156        "gm",
157        "gp",
158        "gq",
159        "gt",
160        "gu",
161        "hm",
162        "ht",
163        "im",
164        "ir",
165        "je",
166        "jm",
167        "jo",
168        "ke",
169        "kh",
170        "km",
171        "kn",
172        "kp",
173        "kz",
174        "ls",
175        "mg",
176        "mh",
177        "mk",
178        "ml",
179        "mm",
180        "mo",
181        "mp",
182        "mq",
183        "ms",
184        "mt",
185        "mu",
186        "mv",
187        "mw",
188        "mz",
189        "ne",
190        "ng",
191        "ni",
192        "np",
193        "nr",
194        "om",
195        "pa",
196        "pf",
197        "pg",
198        "pk",
199        "pn",
200        "ps",
201        "py",
202        "qa",
203        "rw",
204        "sd",
205        "sl",
206        "sm",
207        "so",
208        "sr",
209        "sv",
210        "sy",
211        "sz",
212        "tc",
213        "td",
214        "tg",
215        "tj",
216        "to",
217        "tr",
218        "va",
219        "vg",
220        "vi",
221        "ye",
222        "zm",
223        "zw"
224    );
225
226    protected static final Map<String, String> knownSRV = ImmutableMap.of(
227        "_xmpp-client._tcp.yax.im", "xmpp.yaxim.org",
228        "_xmpps-client._tcp.yax.im", "xmpp.yaxim.org",
229        "_xmpp-server._tcp.yax.im", "xmpp.yaxim.org"
230    );
231
232    public static void init(XmppConnectionService service) {
233        Resolver.SERVICE = service;
234        DnsClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
235        DnsClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
236        DnsClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
237        DnsClient.addDnsServerLookupMechanism(new com.cheogram.android.DnsFallback());
238        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
239        if (client instanceof ReliableDnsClient) {
240            ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
241        }
242        final AbstractDnsClient dnssecclient = DnssecResolverApi.INSTANCE.getClient();
243        if (dnssecclient instanceof ReliableDnsClient) {
244            ((ReliableDnsClient) dnssecclient).setUseHardcodedDnsServers(false);
245        }
246    }
247
248    public static List<Result> fromHardCoded(final String hostname, final int port) {
249        final Result result = new Result();
250        result.hostname = DnsName.from(hostname);
251        result.port = port;
252        result.directTls = useDirectTls(port);
253        result.authenticated = true;
254        return Collections.singletonList(result);
255    }
256
257    public static void checkDomain(final Jid jid) {
258        DnsName.from(jid.getDomain());
259    }
260
261    public static boolean invalidHostname(final String hostname) {
262        try {
263            DnsName.from(hostname);
264            return false;
265        } catch (final InvalidDnsNameException | IllegalArgumentException e) {
266            return true;
267        }
268    }
269
270    public static void clearCache() {
271        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
272        final DnsCache dnsCache = client.getCache();
273        if (dnsCache instanceof LruCache) {
274            Log.d(Config.LOGTAG,"clearing DNS cache");
275            ((LruCache) dnsCache).clear();
276        }
277
278        final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
279        final DnsCache dnsCacheSec = clientSec.getCache();
280        if (dnsCacheSec instanceof LruCache) {
281            Log.d(Config.LOGTAG,"clearing DNSSEC cache");
282            ((LruCache) dnsCacheSec).clear();
283        }
284    }
285
286    public static boolean useDirectTls(final int port) {
287        return port == 443 || port == 5223;
288    }
289
290    public static List<Result> resolve(final String domain) {
291        final List<Result> ipResults = fromIpAddress(domain);
292        if (!ipResults.isEmpty()) {
293            return ipResults;
294        }
295
296        final var startTls = resolveSrvAsFuture(domain, false);
297        final var directTls = resolveSrvAsFuture(domain, true);
298
299        final var combined = merge(ImmutableList.of(startTls, directTls));
300
301        final var combinedWithFallback =
302                Futures.transformAsync(
303                        combined,
304                        results -> {
305                            if (results.isEmpty()) {
306                                return resolveNoSrvAsFuture(DnsName.from(domain), true);
307                            } else {
308                                return Futures.immediateFuture(results);
309                            }
310                        },
311                        MoreExecutors.directExecutor());
312        final var orderedFuture =
313                Futures.transform(
314                        combinedWithFallback,
315                        all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
316                        MoreExecutors.directExecutor());
317        try {
318            final var ordered = orderedFuture.get();
319            Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
320            return ordered;
321        } catch (final ExecutionException e) {
322            Log.d(Config.LOGTAG, "error resolving DNS", e);
323            return Collections.emptyList();
324        } catch (final InterruptedException e) {
325            Log.d(Config.LOGTAG, "DNS resolution interrupted");
326            return Collections.emptyList();
327        }
328    }
329
330    private static List<Result> fromIpAddress(final String domain) {
331        if (IP.matches(domain)) {
332            final InetAddress inetAddress;
333            try {
334                inetAddress = InetAddress.getByName(domain);
335            } catch (final UnknownHostException e) {
336                return Collections.emptyList();
337            }
338            final Result result = new Result();
339            result.ip = inetAddress;
340            result.port = DEFAULT_PORT_XMPP;
341            return Collections.singletonList(result);
342        } else {
343            return Collections.emptyList();
344        }
345    }
346
347    private static ListenableFuture<List<Result>> resolveSrvAsFuture(
348            final String domain, final boolean directTls) {
349        final DnsName dnsName =
350                DnsName.from(
351                        (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
352        final var resultFuture = resolveAsFuture(dnsName, SRV.class);
353        return Futures.transformAsync(
354                resultFuture,
355                result -> resolveIpsAsFuture(result, directTls),
356                MoreExecutors.directExecutor());
357    }
358
359    @NonNull
360    private static ListenableFuture<List<Result>> resolveIpsAsFuture(
361            final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
362        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
363                new ImmutableList.Builder<>();
364        for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
365            if (record.target.length() == 0 && record.priority == 0) {
366                continue;
367            }
368            final var ipv4sRaw =
369                    resolveIpsAsFuture(
370                            record, A.class, srvResolverResult.isAuthenticData(), directTls);
371            final var ipv4s =
372                    Futures.transform(
373                            ipv4sRaw,
374                            results -> {
375                                if (results.isEmpty()) {
376                                    final Result resolverResult =
377                                            Result.fromRecord(record, directTls);
378                                    resolverResult.authenticated =
379                                            srvResolverResult.isAuthenticData();
380                                    return Collections.singletonList(resolverResult);
381                                } else {
382                                    return results;
383                                }
384                            },
385                            MoreExecutors.directExecutor());
386            final var ipv6s =
387                    resolveIpsAsFuture(
388                            record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
389            futuresBuilder.add(ipv4s);
390            futuresBuilder.add(ipv6s);
391        }
392        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
393        return merge(futures);
394    }
395
396    private static ListenableFuture<List<Result>> merge(
397            final Collection<ListenableFuture<List<Result>>> futures) {
398        return Futures.transform(
399                Futures.successfulAsList(futures),
400                lists -> {
401                    final var builder = new ImmutableList.Builder<Result>();
402                    for (final Collection<Result> list : lists) {
403                        if (list == null) {
404                            continue;
405                        }
406                        builder.addAll(list);
407                    }
408                    return builder.build();
409                },
410                MoreExecutors.directExecutor());
411    }
412
413    private static <D extends InternetAddressRR<?>>
414            ListenableFuture<List<Result>> resolveIpsAsFuture(
415                    final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
416        final var resultFuture = resolveAsFuture(srv.target, type);
417        return Futures.transform(
418                resultFuture,
419                result -> {
420                    final var builder = new ImmutableList.Builder<Result>();
421                    for (D record : result.getAnswersOrEmptySet()) {
422                        Result resolverResult = Result.fromRecord(srv, directTls);
423                        resolverResult.authenticated =
424                                result.isAuthenticData()
425                                        && authenticated; // TODO technically it does not matter if
426                        // the IP
427                        // was authenticated
428                        resolverResult.ip = record.getInetAddress();
429                        builder.add(resolverResult);
430                    }
431                    return builder.build();
432                },
433                MoreExecutors.directExecutor());
434    }
435
436    private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
437            final DnsName dnsName, boolean cName) {
438        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
439                new ImmutableList.Builder<>();
440        ListenableFuture<List<Result>> aRecordResults =
441                Futures.transform(
442                        resolveAsFuture(dnsName, A.class),
443                        result ->
444                                Lists.transform(
445                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
446                                        a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())),
447                        MoreExecutors.directExecutor());
448        futuresBuilder.add(aRecordResults);
449        ListenableFuture<List<Result>> aaaaRecordResults =
450                Futures.transform(
451                        resolveAsFuture(dnsName, AAAA.class),
452                        result ->
453                                Lists.transform(
454                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
455                                        aaaa ->
456                                                Result.createDefault(
457                                                        dnsName, aaaa.getInetAddress(), result.isAuthenticData())),
458                        MoreExecutors.directExecutor());
459        futuresBuilder.add(aaaaRecordResults);
460        if (cName) {
461            ListenableFuture<List<Result>> cNameRecordResults =
462                    Futures.transformAsync(
463                            resolveAsFuture(dnsName, CNAME.class),
464                            result -> {
465                                Collection<ListenableFuture<List<Result>>> test =
466                                        Lists.transform(
467                                                ImmutableList.copyOf(result.getAnswersOrEmptySet()),
468                                                cname -> resolveNoSrvAsFuture(cname.target, false));
469                                return merge(test);
470                            },
471                            MoreExecutors.directExecutor());
472            futuresBuilder.add(cNameRecordResults);
473        }
474        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
475        final var noSrvFallbacks = merge(futures);
476        return Futures.transform(
477                noSrvFallbacks,
478                results -> {
479                    if (results.isEmpty()) {
480                        return Collections.singletonList(Result.createDefault(dnsName));
481                    } else {
482                        return results;
483                    }
484                },
485                MoreExecutors.directExecutor());
486    }
487
488    private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
489            final DnsName dnsName, final Class<D> type) {
490        final var start = System.currentTimeMillis();
491        return Futures.submit(
492                () -> {
493                    final Question question = new Question(dnsName, Record.TYPE.getType(type));
494                    if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
495                        for (int i = 0; i < 5; i++) {
496                            if (System.currentTimeMillis() - start > 5000) {
497                                Log.d(Config.LOGTAG, "DNS taking too long, abort DNSSEC retries after " + i + " for " + type.getSimpleName() + " " + dnsName);
498                                break;
499                            }
500                            Log.d(Config.LOGTAG, "DNSSEC try " + i + " for " + type.getSimpleName() + " " + dnsName);
501                            try {
502                                ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
503                                if (result.wasSuccessful() && !result.isAuthenticData()) {
504                                    Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
505                                }
506                                return result;
507                            } catch (DnssecValidationFailedException e) {
508                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + dnsName + " with DNSSEC. Try: " + i, e);
509                                // Try again, may be transient DNSSEC failure https://github.com/MiniDNS/minidns/issues/132
510                                if ("CNAME".equals(type.getSimpleName())) break; // CNAME failure on NXDOMAIN is common don't retry?
511                            } catch (Throwable throwable) {
512                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
513                                break;
514                            }
515                        }
516                    }
517                    return ResolverApi.INSTANCE.resolve(question);
518                },
519                DNS_QUERY_EXECUTOR);
520    }
521
522    public static class Result {
523        public static final String DOMAIN = "domain";
524        public static final String IP = "ip";
525        public static final String HOSTNAME = "hostname";
526        public static final String PORT = "port";
527        public static final String PRIORITY = "priority";
528        public static final String DIRECT_TLS = "directTls";
529        public static final String AUTHENTICATED = "authenticated";
530        private InetAddress ip;
531        private DnsName hostname;
532        private int port = DEFAULT_PORT_XMPP;
533        private boolean directTls = false;
534        private boolean authenticated = false;
535        private int priority;
536
537        static Result fromRecord(final SRV srv, final boolean directTls) {
538            final Result result = new Result();
539            result.port = srv.port;
540            result.hostname = srv.target;
541            result.directTls = directTls;
542            result.priority = srv.priority;
543            return result;
544        }
545
546        static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
547            Result result = new Result();
548            result.port = DEFAULT_PORT_XMPP;
549            result.hostname = hostname;
550            result.ip = ip;
551            result.authenticated = authenticated;
552            return result;
553        }
554
555        static Result createDefault(final DnsName hostname) {
556            return createDefault(hostname, null, false);
557        }
558
559        public static Result fromCursor(final Cursor cursor) {
560            final Result result = new Result();
561            try {
562                result.ip =
563                        InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
564            } catch (final UnknownHostException e) {
565                result.ip = null;
566            }
567            final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
568            result.hostname = hostname == null ? null : DnsName.from(hostname);
569            result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
570            result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
571            result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
572            result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
573            return result;
574        }
575
576        @Override
577        public boolean equals(Object o) {
578            if (this == o) return true;
579            if (o == null || getClass() != o.getClass()) return false;
580            Result result = (Result) o;
581            return port == result.port
582                    && directTls == result.directTls
583                    && authenticated == result.authenticated
584                    && priority == result.priority
585                    && Objects.equal(ip, result.ip)
586                    && Objects.equal(hostname, result.hostname);
587        }
588
589        @Override
590        public int hashCode() {
591            return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
592        }
593
594        public InetAddress getIp() {
595            return ip;
596        }
597
598        public int getPort() {
599            return port;
600        }
601
602        public DnsName getHostname() {
603            return hostname;
604        }
605
606        public boolean isDirectTls() {
607            return directTls;
608        }
609
610        public boolean isAuthenticated() {
611            return authenticated;
612        }
613
614        @Override
615        @NonNull
616        public String toString() {
617            return MoreObjects.toStringHelper(this)
618                    .add("ip", ip)
619                    .add("hostname", hostname)
620                    .add("port", port)
621                    .add("directTls", directTls)
622                    .add("authenticated", authenticated)
623                    .add("priority", priority)
624                    .toString();
625        }
626
627        public ContentValues toContentValues() {
628            final ContentValues contentValues = new ContentValues();
629            contentValues.put(IP, ip == null ? null : ip.getAddress());
630            contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
631            contentValues.put(PORT, port);
632            contentValues.put(PRIORITY, priority);
633            contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
634            contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
635            return contentValues;
636        }
637
638        public Result seeOtherHost(final String seeOtherHost) {
639            final String hostname = seeOtherHost.trim();
640            if (hostname.isEmpty()) {
641                return null;
642            }
643            final Result result = new Result();
644            result.directTls = this.directTls;
645            final int portSegmentStart = hostname.lastIndexOf(':');
646            if (hostname.charAt(hostname.length() - 1) != ']'
647                    && portSegmentStart >= 0
648                    && hostname.length() >= portSegmentStart + 1) {
649                final String hostPart = hostname.substring(0, portSegmentStart);
650                final String portPart = hostname.substring(portSegmentStart + 1);
651                final Integer port = Ints.tryParse(portPart);
652                if (port == null || Strings.isNullOrEmpty(hostPart)) {
653                    return null;
654                }
655                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
656                result.port = port;
657                if (InetAddresses.isInetAddress(host)) {
658                    final InetAddress inetAddress;
659                    try {
660                        inetAddress = InetAddresses.forString(host);
661                    } catch (final IllegalArgumentException e) {
662                        return null;
663                    }
664                    result.ip = inetAddress;
665                } else {
666                    if (hostPart.trim().isEmpty()) {
667                        return null;
668                    }
669                    try {
670                        result.hostname = DnsName.from(hostPart.trim());
671                    } catch (final Exception e) {
672                        return null;
673                    }
674                }
675            } else {
676                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
677                if (InetAddresses.isInetAddress(host)) {
678                    final InetAddress inetAddress;
679                    try {
680                        inetAddress = InetAddresses.forString(host);
681                    } catch (final IllegalArgumentException e) {
682                        return null;
683                    }
684                    result.ip = inetAddress;
685                } else {
686                    try {
687                        result.hostname = DnsName.from(hostname);
688                    } catch (final Exception e) {
689                        return null;
690                    }
691                }
692                result.port = port;
693            }
694            return result;
695        }
696    }
697}