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