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