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.Strings;
 10import com.google.common.base.Throwables;
 11import com.google.common.collect.ImmutableMap;
 12import com.google.common.net.InetAddresses;
 13import com.google.common.primitives.Ints;
 14
 15import java.io.IOException;
 16import java.lang.reflect.Field;
 17import java.net.Inet4Address;
 18import java.net.InetAddress;
 19import java.net.UnknownHostException;
 20import java.util.Arrays;
 21import java.util.ArrayList;
 22import java.util.Collections;
 23import java.util.List;
 24import java.util.Map;
 25
 26//import de.gultsch.minidns.AndroidDNSClient;
 27import org.minidns.AbstractDnsClient;
 28import org.minidns.DnsCache;
 29import org.minidns.DnsClient;
 30import org.minidns.cache.LruCache;
 31import org.minidns.dnsmessage.Question;
 32import org.minidns.dnsname.DnsName;
 33import org.minidns.dnssec.DnssecResultNotAuthenticException;
 34import org.minidns.dnssec.DnssecValidationFailedException;
 35import org.minidns.dnsserverlookup.AndroidUsingExec;
 36import org.minidns.hla.DnssecResolverApi;
 37import org.minidns.hla.ResolverApi;
 38import org.minidns.hla.ResolverResult;
 39import org.minidns.iterative.ReliableDnsClient;
 40import org.minidns.record.A;
 41import org.minidns.record.AAAA;
 42import org.minidns.record.CNAME;
 43import org.minidns.record.Data;
 44import org.minidns.record.InternetAddressRR;
 45import org.minidns.record.Record;
 46import org.minidns.record.SRV;
 47import eu.siacs.conversations.Config;
 48import eu.siacs.conversations.R;
 49import eu.siacs.conversations.services.XmppConnectionService;
 50import eu.siacs.conversations.xmpp.Jid;
 51
 52public class Resolver {
 53
 54    public static final int DEFAULT_PORT_XMPP = 5222;
 55
 56    private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
 57    private static final String STARTTLS_SERVICE = "_xmpp-client";
 58
 59    private static XmppConnectionService SERVICE = null;
 60
 61    private static List<String> DNSSECLESS_TLDS = Arrays.asList(
 62        "ae",
 63        "aero",
 64        "ai",
 65        "al",
 66        "ao",
 67        "aq",
 68        "as",
 69        "ba",
 70        "bb",
 71        "bd",
 72        "bf",
 73        "bi",
 74        "bj",
 75        "bn",
 76        "bo",
 77        "bs",
 78        "bw",
 79        "cd",
 80        "cf",
 81        "cg",
 82        "ci",
 83        "ck",
 84        "cm",
 85        "cu",
 86        "cv",
 87        "cw",
 88        "dj",
 89        "dm",
 90        "do",
 91        "ec",
 92        "eg",
 93        "eh",
 94        "er",
 95        "et",
 96        "fj",
 97        "fk",
 98        "ga",
 99        "ge",
100        "gf",
101        "gh",
102        "gm",
103        "gp",
104        "gq",
105        "gt",
106        "gu",
107        "hm",
108        "ht",
109        "im",
110        "ir",
111        "je",
112        "jm",
113        "jo",
114        "ke",
115        "kh",
116        "km",
117        "kn",
118        "kp",
119        "kz",
120        "ls",
121        "mg",
122        "mh",
123        "mk",
124        "ml",
125        "mm",
126        "mo",
127        "mp",
128        "mq",
129        "ms",
130        "mt",
131        "mu",
132        "mv",
133        "mw",
134        "mz",
135        "ne",
136        "ng",
137        "ni",
138        "np",
139        "nr",
140        "om",
141        "pa",
142        "pf",
143        "pg",
144        "pk",
145        "pn",
146        "ps",
147        "py",
148        "qa",
149        "rw",
150        "sd",
151        "sl",
152        "sm",
153        "so",
154        "sr",
155        "sv",
156        "sy",
157        "sz",
158        "tc",
159        "td",
160        "tg",
161        "tj",
162        "to",
163        "tr",
164        "va",
165        "vg",
166        "vi",
167        "ye",
168        "zm",
169        "zw"
170    );
171
172    protected static final Map<String, String> knownSRV = ImmutableMap.of(
173        "_xmpp-client._tcp.yax.im", "xmpp.yaxim.org",
174        "_xmpps-client._tcp.yax.im", "xmpp.yaxim.org",
175        "_xmpp-server._tcp.yax.im", "xmpp.yaxim.org"
176    );
177
178    public static void init(XmppConnectionService service) {
179        Resolver.SERVICE = service;
180        DnsClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
181        DnsClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
182        DnsClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
183        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
184        if (client instanceof ReliableDnsClient) {
185            ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
186        }
187    }
188
189    public static List<Result> fromHardCoded(final String hostname, final int port) {
190        final Result result = new Result();
191        result.hostname = DnsName.from(hostname);
192        result.port = port;
193        result.directTls = useDirectTls(port);
194        result.authenticated = true;
195        return Collections.singletonList(result);
196    }
197
198    public static void checkDomain(final Jid jid) {
199        DnsName.from(jid.getDomain());
200    }
201
202    public static boolean invalidHostname(final String hostname) {
203        try {
204            DnsName.from(hostname);
205            return false;
206        } catch (IllegalArgumentException e) {
207            return true;
208        }
209    }
210
211    public static void clearCache() {
212        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
213        final DnsCache dnsCache = client.getCache();
214        if (dnsCache instanceof LruCache) {
215            Log.d(Config.LOGTAG,"clearing DNS cache");
216            ((LruCache) dnsCache).clear();
217        }
218
219        final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
220        final DnsCache dnsCacheSec = clientSec.getCache();
221        if (dnsCacheSec instanceof LruCache) {
222            Log.d(Config.LOGTAG,"clearing DNSSEC cache");
223            ((LruCache) dnsCacheSec).clear();
224        }
225    }
226
227
228    public static boolean useDirectTls(final int port) {
229        return port == 443 || port == 5223;
230    }
231
232    public static List<Result> resolve(final String domain) {
233        final  List<Result> ipResults = fromIpAddress(domain);
234        if (ipResults.size() > 0) {
235            return ipResults;
236        }
237        final List<Result> results = new ArrayList<>();
238        final List<Result> fallbackResults = new ArrayList<>();
239        final Thread[] threads = new Thread[3];
240        threads[0] = new Thread(() -> {
241            try {
242                final List<Result> list = resolveSrv(domain, true);
243                synchronized (results) {
244                    results.addAll(list);
245                }
246            } catch (final Throwable throwable) {
247                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
248                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
249                }
250            }
251        });
252        threads[1] = new Thread(() -> {
253            try {
254                final List<Result> list = resolveSrv(domain, false);
255                synchronized (results) {
256                    results.addAll(list);
257                }
258            } catch (final Throwable throwable) {
259                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
260                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
261                }
262            }
263        });
264        threads[2] = new Thread(() -> {
265            List<Result> list = resolveNoSrvRecords(DnsName.from(domain), true);
266            synchronized (fallbackResults) {
267                fallbackResults.addAll(list);
268            }
269        });
270        for (final Thread thread : threads) {
271            thread.start();
272        }
273        try {
274            threads[0].join();
275            threads[1].join();
276            if (results.size() > 0) {
277                threads[2].interrupt();
278                synchronized (results) {
279                    Collections.sort(results);
280                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
281                    return results;
282                }
283            } else {
284                threads[2].join();
285                synchronized (fallbackResults) {
286                    Collections.sort(fallbackResults);
287                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
288                    return fallbackResults;
289                }
290            }
291        } catch (InterruptedException e) {
292            for (Thread thread : threads) {
293                thread.interrupt();
294            }
295            return Collections.emptyList();
296        }
297    }
298
299    private static List<Result> fromIpAddress(String domain) {
300        if (!IP.matches(domain)) {
301            return Collections.emptyList();
302        }
303        try {
304            Result result = new Result();
305            result.ip = InetAddress.getByName(domain);
306            result.port = DEFAULT_PORT_XMPP;
307            result.authenticated = true;
308            return Collections.singletonList(result);
309        } catch (UnknownHostException e) {
310            return Collections.emptyList();
311        }
312    }
313
314    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
315        final String dnsNameS = (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain;
316        DnsName dnsName = DnsName.from(dnsNameS);
317        ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
318        final List<Result> results = new ArrayList<>();
319        final List<Thread> threads = new ArrayList<>();
320        for (SRV record : result.getAnswersOrEmptySet()) {
321            if (record.name.length() == 0 && record.priority == 0) {
322                continue;
323            }
324            final boolean authentic = result.isAuthenticData() || record.target.toString().equals(knownSRV.get(dnsNameS));
325            threads.add(new Thread(() -> {
326                final List<Result> ipv4s = resolveIp(record, A.class, authentic, directTls);
327                if (ipv4s.size() == 0) {
328                    Result resolverResult = Result.fromRecord(record, directTls);
329                    resolverResult.authenticated = result.isAuthenticData();
330                    ipv4s.add(resolverResult);
331                }
332                synchronized (results) {
333                    results.addAll(ipv4s);
334                }
335
336            }));
337            threads.add(new Thread(() -> {
338                final List<Result> ipv6s = resolveIp(record, AAAA.class, authentic, directTls);
339                synchronized (results) {
340                    results.addAll(ipv6s);
341                }
342            }));
343        }
344        for (Thread thread : threads) {
345            thread.start();
346        }
347        for (Thread thread : threads) {
348            try {
349                thread.join();
350            } catch (InterruptedException e) {
351                return Collections.emptyList();
352            }
353        }
354        return results;
355    }
356
357    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
358        List<Result> list = new ArrayList<>();
359        try {
360            ResolverResult<D> results = resolveWithFallback(srv.target, type);
361            for (D record : results.getAnswersOrEmptySet()) {
362                Result resolverResult = Result.fromRecord(srv, directTls);
363                resolverResult.authenticated = results.isAuthenticData() && authenticated;
364                resolverResult.ip = record.getInetAddress();
365                list.add(resolverResult);
366            }
367        } catch (Throwable t) {
368            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
369        }
370        return list;
371    }
372
373    private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
374        final List<Result> results = new ArrayList<>();
375        try {
376            ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
377            for (A a : aResult.getAnswersOrEmptySet()) {
378                Result r = Result.createDefault(dnsName, a.getInetAddress());
379                r.authenticated = aResult.isAuthenticData();
380                results.add(r);
381            }
382            ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
383            for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
384                Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
385                r.authenticated = aaaaResult.isAuthenticData();
386                results.add(r);
387            }
388            if (results.size() == 0 && withCnames) {
389                ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
390                for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
391                    for (Result r : resolveNoSrvRecords(cname.name, false)) {
392                        r.authenticated = r.authenticated && cnameResult.isAuthenticData();
393                        results.add(r);
394                    }
395                }
396            }
397        } catch (final Throwable throwable) {
398            if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
399                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
400            }
401        }
402        results.add(Result.createDefault(dnsName));
403        return results;
404    }
405
406    private static <D extends Data> ResolverResult<D> resolveWithFallback(DnsName dnsName, Class<D> type) throws IOException {
407        final Question question = new Question(dnsName, Record.TYPE.getType(type));
408        if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
409            try {
410                ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
411                if (result.wasSuccessful() && !result.isAuthenticData()) {
412                    Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
413                }
414                return result;
415            } catch (DnssecValidationFailedException e) {
416                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
417            } catch (IOException e) {
418                throw e;
419            } catch (Throwable throwable) {
420                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
421            }
422        }
423        return ResolverApi.INSTANCE.resolve(question);
424    }
425
426    public static class Result implements Comparable<Result> {
427        public static final String DOMAIN = "domain";
428        public static final String IP = "ip";
429        public static final String HOSTNAME = "hostname";
430        public static final String PORT = "port";
431        public static final String PRIORITY = "priority";
432        public static final String DIRECT_TLS = "directTls";
433        public static final String AUTHENTICATED = "authenticated";
434        private InetAddress ip;
435        private DnsName hostname;
436        private int port = DEFAULT_PORT_XMPP;
437        private boolean directTls = false;
438        private boolean authenticated = false;
439        private int priority;
440
441        static Result fromRecord(SRV srv, boolean directTls) {
442            Result result = new Result();
443            result.port = srv.port;
444            result.hostname = srv.name;
445            result.directTls = directTls;
446            result.priority = srv.priority;
447            return result;
448        }
449
450        static Result createDefault(DnsName hostname, InetAddress ip) {
451            Result result = new Result();
452            result.port = DEFAULT_PORT_XMPP;
453            result.hostname = hostname;
454            result.ip = ip;
455            return result;
456        }
457
458        static Result createDefault(DnsName hostname) {
459            return createDefault(hostname, null);
460        }
461
462        public static Result fromCursor(Cursor cursor) {
463            final Result result = new Result();
464            try {
465                result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
466            } catch (UnknownHostException e) {
467                result.ip = null;
468            }
469            final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
470            result.hostname = hostname == null ? null : DnsName.from(hostname);
471            result.port = cursor.getInt(cursor.getColumnIndex(PORT));
472            result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
473            result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
474            result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
475            return result;
476        }
477
478        @Override
479        public boolean equals(Object o) {
480            if (this == o) return true;
481            if (o == null || getClass() != o.getClass()) return false;
482
483            Result result = (Result) o;
484
485            if (port != result.port) return false;
486            if (directTls != result.directTls) return false;
487            if (authenticated != result.authenticated) return false;
488            if (priority != result.priority) return false;
489            if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
490            return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
491        }
492
493        @Override
494        public int hashCode() {
495            int result = ip != null ? ip.hashCode() : 0;
496            result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
497            result = 31 * result + port;
498            result = 31 * result + (directTls ? 1 : 0);
499            result = 31 * result + (authenticated ? 1 : 0);
500            result = 31 * result + priority;
501            return result;
502        }
503
504        public InetAddress getIp() {
505            return ip;
506        }
507
508        public int getPort() {
509            return port;
510        }
511
512        public DnsName getHostname() {
513            return hostname;
514        }
515
516        public boolean isDirectTls() {
517            return directTls;
518        }
519
520        public boolean isAuthenticated() {
521            return authenticated;
522        }
523
524        @Override
525        public String toString() {
526            return "Result{" +
527                    "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
528                    ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' +
529                    ", port=" + port +
530                    ", directTls=" + directTls +
531                    ", authenticated=" + authenticated +
532                    ", priority=" + priority +
533                    '}';
534        }
535
536        @Override
537        public int compareTo(@NonNull Result result) {
538            if (result.priority == priority) {
539                if (directTls == result.directTls) {
540                    if (ip == null && result.ip == null) {
541                        return 0;
542                    } else if (ip != null && result.ip != null) {
543                        if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
544                            return 0;
545                        } else {
546                            return ip instanceof Inet4Address ? -1 : 1;
547                        }
548                    } else {
549                        return ip != null ? -1 : 1;
550                    }
551                } else {
552                    return directTls ? 1 : -1;
553                }
554            } else {
555                return priority - result.priority;
556            }
557        }
558
559        public ContentValues toContentValues() {
560            final ContentValues contentValues = new ContentValues();
561            contentValues.put(IP, ip == null ? null : ip.getAddress());
562            contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
563            contentValues.put(PORT, port);
564            contentValues.put(PRIORITY, priority);
565            contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
566            contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
567            return contentValues;
568        }
569
570        public Result seeOtherHost(final String seeOtherHost) {
571            final String hostname = seeOtherHost.trim();
572            if (hostname.isEmpty()) {
573                return null;
574            }
575            final Result result = new Result();
576            result.directTls = this.directTls;
577            final int portSegmentStart = hostname.lastIndexOf(':');
578            if (hostname.charAt(hostname.length() - 1) != ']'
579                    && portSegmentStart >= 0
580                    && hostname.length() >= portSegmentStart + 1) {
581                final String hostPart = hostname.substring(0, portSegmentStart);
582                final String portPart = hostname.substring(portSegmentStart + 1);
583                final Integer port = Ints.tryParse(portPart);
584                if (port == null || Strings.isNullOrEmpty(hostPart)) {
585                    return null;
586                }
587                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
588                result.port = port;
589                if (InetAddresses.isInetAddress(host)) {
590                    final InetAddress inetAddress;
591                    try {
592                        inetAddress = InetAddresses.forString(host);
593                    } catch (final IllegalArgumentException e) {
594                        return null;
595                    }
596                    result.ip = inetAddress;
597                } else {
598                    if (hostPart.trim().isEmpty()) {
599                        return null;
600                    }
601                    try {
602                        result.hostname = DnsName.from(hostPart.trim());
603                    } catch (final Exception e) {
604                        return null;
605                    }
606                }
607            } else {
608                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
609                if (InetAddresses.isInetAddress(host)) {
610                    final InetAddress inetAddress;
611                    try {
612                        inetAddress = InetAddresses.forString(host);
613                    } catch (final IllegalArgumentException e) {
614                        return null;
615                    }
616                    result.ip = inetAddress;
617                } else {
618                    try {
619                        result.hostname = DnsName.from(hostname);
620                    } catch (final Exception e) {
621                        return null;
622                    }
623                }
624                result.port = port;
625            }
626            return result;
627        }
628    }
629
630}