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        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
238        if (client instanceof ReliableDnsClient) {
239            ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
240        }
241        final AbstractDnsClient dnssecclient = DnssecResolverApi.INSTANCE.getClient();
242        if (dnssecclient instanceof ReliableDnsClient) {
243            ((ReliableDnsClient) dnssecclient).setUseHardcodedDnsServers(false);
244        }
245    }
246
247    public static List<Result> fromHardCoded(final String hostname, final int port) {
248        final Result result = new Result();
249        result.hostname = DnsName.from(hostname);
250        result.port = port;
251        result.directTls = useDirectTls(port);
252        result.authenticated = true;
253        return Collections.singletonList(result);
254    }
255
256    public static void checkDomain(final Jid jid) {
257        DnsName.from(jid.getDomain());
258    }
259
260    public static boolean invalidHostname(final String hostname) {
261        try {
262            DnsName.from(hostname);
263            return false;
264        } catch (final InvalidDnsNameException | IllegalArgumentException e) {
265            return true;
266        }
267    }
268
269    public static void clearCache() {
270        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
271        final DnsCache dnsCache = client.getCache();
272        if (dnsCache instanceof LruCache) {
273            Log.d(Config.LOGTAG,"clearing DNS cache");
274            ((LruCache) dnsCache).clear();
275        }
276
277        final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
278        final DnsCache dnsCacheSec = clientSec.getCache();
279        if (dnsCacheSec instanceof LruCache) {
280            Log.d(Config.LOGTAG,"clearing DNSSEC cache");
281            ((LruCache) dnsCacheSec).clear();
282        }
283    }
284
285    public static boolean useDirectTls(final int port) {
286        return port == 443 || port == 5223;
287    }
288
289    public static List<Result> resolve(final String domain) {
290        final List<Result> ipResults = fromIpAddress(domain);
291        if (!ipResults.isEmpty()) {
292            return ipResults;
293        }
294
295        final var startTls = resolveSrvAsFuture(domain, false);
296        final var directTls = resolveSrvAsFuture(domain, true);
297
298        final var combined = merge(ImmutableList.of(startTls, directTls));
299
300        final var combinedWithFallback =
301                Futures.transformAsync(
302                        combined,
303                        results -> {
304                            if (results.isEmpty()) {
305                                return resolveNoSrvAsFuture(DnsName.from(domain), true);
306                            } else {
307                                return Futures.immediateFuture(results);
308                            }
309                        },
310                        MoreExecutors.directExecutor());
311        final var orderedFuture =
312                Futures.transform(
313                        combinedWithFallback,
314                        all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
315                        MoreExecutors.directExecutor());
316        try {
317            final var ordered = orderedFuture.get();
318            Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
319            return ordered;
320        } catch (final ExecutionException e) {
321            Log.d(Config.LOGTAG, "error resolving DNS", e);
322            return Collections.emptyList();
323        } catch (final InterruptedException e) {
324            Log.d(Config.LOGTAG, "DNS resolution interrupted");
325            return Collections.emptyList();
326        }
327    }
328
329    private static List<Result> fromIpAddress(final String domain) {
330        if (IP.matches(domain)) {
331            final InetAddress inetAddress;
332            try {
333                inetAddress = InetAddress.getByName(domain);
334            } catch (final UnknownHostException e) {
335                return Collections.emptyList();
336            }
337            final Result result = new Result();
338            result.ip = inetAddress;
339            result.port = DEFAULT_PORT_XMPP;
340            return Collections.singletonList(result);
341        } else {
342            return Collections.emptyList();
343        }
344    }
345
346    private static ListenableFuture<List<Result>> resolveSrvAsFuture(
347            final String domain, final boolean directTls) {
348        final DnsName dnsName =
349                DnsName.from(
350                        (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
351        final var resultFuture = resolveAsFuture(dnsName, SRV.class);
352        return Futures.transformAsync(
353                resultFuture,
354                result -> resolveIpsAsFuture(result, directTls),
355                MoreExecutors.directExecutor());
356    }
357
358    @NonNull
359    private static ListenableFuture<List<Result>> resolveIpsAsFuture(
360            final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
361        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
362                new ImmutableList.Builder<>();
363        for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
364            if (record.target.length() == 0 && record.priority == 0) {
365                continue;
366            }
367            final var ipv4sRaw =
368                    resolveIpsAsFuture(
369                            record, A.class, srvResolverResult.isAuthenticData(), directTls);
370            final var ipv4s =
371                    Futures.transform(
372                            ipv4sRaw,
373                            results -> {
374                                if (results.isEmpty()) {
375                                    final Result resolverResult =
376                                            Result.fromRecord(record, directTls);
377                                    resolverResult.authenticated =
378                                            srvResolverResult.isAuthenticData();
379                                    return Collections.singletonList(resolverResult);
380                                } else {
381                                    return results;
382                                }
383                            },
384                            MoreExecutors.directExecutor());
385            final var ipv6s =
386                    resolveIpsAsFuture(
387                            record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
388            futuresBuilder.add(ipv4s);
389            futuresBuilder.add(ipv6s);
390        }
391        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
392        return merge(futures);
393    }
394
395    private static ListenableFuture<List<Result>> merge(
396            final Collection<ListenableFuture<List<Result>>> futures) {
397        return Futures.transform(
398                Futures.successfulAsList(futures),
399                lists -> {
400                    final var builder = new ImmutableList.Builder<Result>();
401                    for (final Collection<Result> list : lists) {
402                        if (list == null) {
403                            continue;
404                        }
405                        builder.addAll(list);
406                    }
407                    return builder.build();
408                },
409                MoreExecutors.directExecutor());
410    }
411
412    private static <D extends InternetAddressRR<?>>
413            ListenableFuture<List<Result>> resolveIpsAsFuture(
414                    final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
415        final var resultFuture = resolveAsFuture(srv.target, type);
416        return Futures.transform(
417                resultFuture,
418                result -> {
419                    final var builder = new ImmutableList.Builder<Result>();
420                    for (D record : result.getAnswersOrEmptySet()) {
421                        Result resolverResult = Result.fromRecord(srv, directTls);
422                        resolverResult.authenticated =
423                                result.isAuthenticData()
424                                        && authenticated; // TODO technically it does not matter if
425                        // the IP
426                        // was authenticated
427                        resolverResult.ip = record.getInetAddress();
428                        builder.add(resolverResult);
429                    }
430                    return builder.build();
431                },
432                MoreExecutors.directExecutor());
433    }
434
435    private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
436            final DnsName dnsName, boolean cName) {
437        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
438                new ImmutableList.Builder<>();
439        ListenableFuture<List<Result>> aRecordResults =
440                Futures.transform(
441                        resolveAsFuture(dnsName, A.class),
442                        result ->
443                                Lists.transform(
444                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
445                                        a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())),
446                        MoreExecutors.directExecutor());
447        futuresBuilder.add(aRecordResults);
448        ListenableFuture<List<Result>> aaaaRecordResults =
449                Futures.transform(
450                        resolveAsFuture(dnsName, AAAA.class),
451                        result ->
452                                Lists.transform(
453                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
454                                        aaaa ->
455                                                Result.createDefault(
456                                                        dnsName, aaaa.getInetAddress(), result.isAuthenticData())),
457                        MoreExecutors.directExecutor());
458        futuresBuilder.add(aaaaRecordResults);
459        if (cName) {
460            ListenableFuture<List<Result>> cNameRecordResults =
461                    Futures.transformAsync(
462                            resolveAsFuture(dnsName, CNAME.class),
463                            result -> {
464                                Collection<ListenableFuture<List<Result>>> test =
465                                        Lists.transform(
466                                                ImmutableList.copyOf(result.getAnswersOrEmptySet()),
467                                                cname -> resolveNoSrvAsFuture(cname.target, false));
468                                return merge(test);
469                            },
470                            MoreExecutors.directExecutor());
471            futuresBuilder.add(cNameRecordResults);
472        }
473        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
474        final var noSrvFallbacks = merge(futures);
475        return Futures.transform(
476                noSrvFallbacks,
477                results -> {
478                    if (results.isEmpty()) {
479                        return Collections.singletonList(Result.createDefault(dnsName));
480                    } else {
481                        return results;
482                    }
483                },
484                MoreExecutors.directExecutor());
485    }
486
487    private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
488            final DnsName dnsName, final Class<D> type) {
489        final var start = System.currentTimeMillis();
490        return Futures.submit(
491                () -> {
492                    final Question question = new Question(dnsName, Record.TYPE.getType(type));
493                    if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
494                        for (int i = 0; i < 5; i++) {
495                            if (System.currentTimeMillis() - start > 5000) break;
496                            try {
497                                ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
498                                if (result.wasSuccessful() && !result.isAuthenticData()) {
499                                    Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
500                                }
501                                return result;
502                            } catch (DnssecValidationFailedException e) {
503                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
504                                // Try again, may be transient DNSSEC failure https://github.com/MiniDNS/minidns/issues/132
505                            } catch (Throwable throwable) {
506                                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
507                                break;
508                            }
509                        }
510                    }
511                    return ResolverApi.INSTANCE.resolve(question);
512                },
513                DNS_QUERY_EXECUTOR);
514    }
515
516    public static class Result {
517        public static final String DOMAIN = "domain";
518        public static final String IP = "ip";
519        public static final String HOSTNAME = "hostname";
520        public static final String PORT = "port";
521        public static final String PRIORITY = "priority";
522        public static final String DIRECT_TLS = "directTls";
523        public static final String AUTHENTICATED = "authenticated";
524        private InetAddress ip;
525        private DnsName hostname;
526        private int port = DEFAULT_PORT_XMPP;
527        private boolean directTls = false;
528        private boolean authenticated = false;
529        private int priority;
530
531        static Result fromRecord(final SRV srv, final boolean directTls) {
532            final Result result = new Result();
533            result.port = srv.port;
534            result.hostname = srv.target;
535            result.directTls = directTls;
536            result.priority = srv.priority;
537            return result;
538        }
539
540        static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
541            Result result = new Result();
542            result.port = DEFAULT_PORT_XMPP;
543            result.hostname = hostname;
544            result.ip = ip;
545            result.authenticated = authenticated;
546            return result;
547        }
548
549        static Result createDefault(final DnsName hostname) {
550            return createDefault(hostname, null, false);
551        }
552
553        public static Result fromCursor(final Cursor cursor) {
554            final Result result = new Result();
555            try {
556                result.ip =
557                        InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
558            } catch (final UnknownHostException e) {
559                result.ip = null;
560            }
561            final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
562            result.hostname = hostname == null ? null : DnsName.from(hostname);
563            result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
564            result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
565            result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
566            result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
567            return result;
568        }
569
570        @Override
571        public boolean equals(Object o) {
572            if (this == o) return true;
573            if (o == null || getClass() != o.getClass()) return false;
574            Result result = (Result) o;
575            return port == result.port
576                    && directTls == result.directTls
577                    && authenticated == result.authenticated
578                    && priority == result.priority
579                    && Objects.equal(ip, result.ip)
580                    && Objects.equal(hostname, result.hostname);
581        }
582
583        @Override
584        public int hashCode() {
585            return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
586        }
587
588        public InetAddress getIp() {
589            return ip;
590        }
591
592        public int getPort() {
593            return port;
594        }
595
596        public DnsName getHostname() {
597            return hostname;
598        }
599
600        public boolean isDirectTls() {
601            return directTls;
602        }
603
604        public boolean isAuthenticated() {
605            return authenticated;
606        }
607
608        @Override
609        @NonNull
610        public String toString() {
611            return MoreObjects.toStringHelper(this)
612                    .add("ip", ip)
613                    .add("hostname", hostname)
614                    .add("port", port)
615                    .add("directTls", directTls)
616                    .add("authenticated", authenticated)
617                    .add("priority", priority)
618                    .toString();
619        }
620
621        public ContentValues toContentValues() {
622            final ContentValues contentValues = new ContentValues();
623            contentValues.put(IP, ip == null ? null : ip.getAddress());
624            contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
625            contentValues.put(PORT, port);
626            contentValues.put(PRIORITY, priority);
627            contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
628            contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
629            return contentValues;
630        }
631
632        public Result seeOtherHost(final String seeOtherHost) {
633            final String hostname = seeOtherHost.trim();
634            if (hostname.isEmpty()) {
635                return null;
636            }
637            final Result result = new Result();
638            result.directTls = this.directTls;
639            final int portSegmentStart = hostname.lastIndexOf(':');
640            if (hostname.charAt(hostname.length() - 1) != ']'
641                    && portSegmentStart >= 0
642                    && hostname.length() >= portSegmentStart + 1) {
643                final String hostPart = hostname.substring(0, portSegmentStart);
644                final String portPart = hostname.substring(portSegmentStart + 1);
645                final Integer port = Ints.tryParse(portPart);
646                if (port == null || Strings.isNullOrEmpty(hostPart)) {
647                    return null;
648                }
649                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
650                result.port = port;
651                if (InetAddresses.isInetAddress(host)) {
652                    final InetAddress inetAddress;
653                    try {
654                        inetAddress = InetAddresses.forString(host);
655                    } catch (final IllegalArgumentException e) {
656                        return null;
657                    }
658                    result.ip = inetAddress;
659                } else {
660                    if (hostPart.trim().isEmpty()) {
661                        return null;
662                    }
663                    try {
664                        result.hostname = DnsName.from(hostPart.trim());
665                    } catch (final Exception e) {
666                        return null;
667                    }
668                }
669            } else {
670                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
671                if (InetAddresses.isInetAddress(host)) {
672                    final InetAddress inetAddress;
673                    try {
674                        inetAddress = InetAddresses.forString(host);
675                    } catch (final IllegalArgumentException e) {
676                        return null;
677                    }
678                    result.ip = inetAddress;
679                } else {
680                    try {
681                        result.hostname = DnsName.from(hostname);
682                    } catch (final Exception e) {
683                        return null;
684                    }
685                }
686                result.port = port;
687            }
688            return result;
689        }
690    }
691}