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