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