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) break;
497                            try {
498                                ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
499                                if (result.wasSuccessful() && !result.isAuthenticData()) {
500                                    Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
501                                }
502                                return result;
503                            } catch (DnssecValidationFailedException e) {
504                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
505                                // Try again, may be transient DNSSEC failure https://github.com/MiniDNS/minidns/issues/132
506                            } catch (Throwable throwable) {
507                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
508                                break;
509                            }
510                        }
511                    }
512                    return ResolverApi.INSTANCE.resolve(question);
513                },
514                DNS_QUERY_EXECUTOR);
515    }
516
517    public static class Result {
518        public static final String DOMAIN = "domain";
519        public static final String IP = "ip";
520        public static final String HOSTNAME = "hostname";
521        public static final String PORT = "port";
522        public static final String PRIORITY = "priority";
523        public static final String DIRECT_TLS = "directTls";
524        public static final String AUTHENTICATED = "authenticated";
525        private InetAddress ip;
526        private DnsName hostname;
527        private int port = DEFAULT_PORT_XMPP;
528        private boolean directTls = false;
529        private boolean authenticated = false;
530        private int priority;
531
532        static Result fromRecord(final SRV srv, final boolean directTls) {
533            final Result result = new Result();
534            result.port = srv.port;
535            result.hostname = srv.target;
536            result.directTls = directTls;
537            result.priority = srv.priority;
538            return result;
539        }
540
541        static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
542            Result result = new Result();
543            result.port = DEFAULT_PORT_XMPP;
544            result.hostname = hostname;
545            result.ip = ip;
546            result.authenticated = authenticated;
547            return result;
548        }
549
550        static Result createDefault(final DnsName hostname) {
551            return createDefault(hostname, null, false);
552        }
553
554        public static Result fromCursor(final Cursor cursor) {
555            final Result result = new Result();
556            try {
557                result.ip =
558                        InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
559            } catch (final UnknownHostException e) {
560                result.ip = null;
561            }
562            final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
563            result.hostname = hostname == null ? null : DnsName.from(hostname);
564            result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
565            result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
566            result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
567            result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
568            return result;
569        }
570
571        @Override
572        public boolean equals(Object o) {
573            if (this == o) return true;
574            if (o == null || getClass() != o.getClass()) return false;
575            Result result = (Result) o;
576            return port == result.port
577                    && directTls == result.directTls
578                    && authenticated == result.authenticated
579                    && priority == result.priority
580                    && Objects.equal(ip, result.ip)
581                    && Objects.equal(hostname, result.hostname);
582        }
583
584        @Override
585        public int hashCode() {
586            return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
587        }
588
589        public InetAddress getIp() {
590            return ip;
591        }
592
593        public int getPort() {
594            return port;
595        }
596
597        public DnsName getHostname() {
598            return hostname;
599        }
600
601        public boolean isDirectTls() {
602            return directTls;
603        }
604
605        public boolean isAuthenticated() {
606            return authenticated;
607        }
608
609        @Override
610        @NonNull
611        public String toString() {
612            return MoreObjects.toStringHelper(this)
613                    .add("ip", ip)
614                    .add("hostname", hostname)
615                    .add("port", port)
616                    .add("directTls", directTls)
617                    .add("authenticated", authenticated)
618                    .add("priority", priority)
619                    .toString();
620        }
621
622        public ContentValues toContentValues() {
623            final ContentValues contentValues = new ContentValues();
624            contentValues.put(IP, ip == null ? null : ip.getAddress());
625            contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
626            contentValues.put(PORT, port);
627            contentValues.put(PRIORITY, priority);
628            contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
629            contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
630            return contentValues;
631        }
632
633        public Result seeOtherHost(final String seeOtherHost) {
634            final String hostname = seeOtherHost.trim();
635            if (hostname.isEmpty()) {
636                return null;
637            }
638            final Result result = new Result();
639            result.directTls = this.directTls;
640            final int portSegmentStart = hostname.lastIndexOf(':');
641            if (hostname.charAt(hostname.length() - 1) != ']'
642                    && portSegmentStart >= 0
643                    && hostname.length() >= portSegmentStart + 1) {
644                final String hostPart = hostname.substring(0, portSegmentStart);
645                final String portPart = hostname.substring(portSegmentStart + 1);
646                final Integer port = Ints.tryParse(portPart);
647                if (port == null || Strings.isNullOrEmpty(hostPart)) {
648                    return null;
649                }
650                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
651                result.port = port;
652                if (InetAddresses.isInetAddress(host)) {
653                    final InetAddress inetAddress;
654                    try {
655                        inetAddress = InetAddresses.forString(host);
656                    } catch (final IllegalArgumentException e) {
657                        return null;
658                    }
659                    result.ip = inetAddress;
660                } else {
661                    if (hostPart.trim().isEmpty()) {
662                        return null;
663                    }
664                    try {
665                        result.hostname = DnsName.from(hostPart.trim());
666                    } catch (final Exception e) {
667                        return null;
668                    }
669                }
670            } else {
671                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
672                if (InetAddresses.isInetAddress(host)) {
673                    final InetAddress inetAddress;
674                    try {
675                        inetAddress = InetAddresses.forString(host);
676                    } catch (final IllegalArgumentException e) {
677                        return null;
678                    }
679                    result.ip = inetAddress;
680                } else {
681                    try {
682                        result.hostname = DnsName.from(hostname);
683                    } catch (final Exception e) {
684                        return null;
685                    }
686                }
687                result.port = port;
688            }
689            return result;
690        }
691    }
692}