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