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