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