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