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 final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
238 if (client instanceof ReliableDnsClient) {
239 ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
240 }
241 final AbstractDnsClient dnssecclient = DnssecResolverApi.INSTANCE.getClient();
242 if (dnssecclient instanceof ReliableDnsClient) {
243 ((ReliableDnsClient) dnssecclient).setUseHardcodedDnsServers(false);
244 }
245 }
246
247 public static List<Result> fromHardCoded(final String hostname, final int port) {
248 final Result result = new Result();
249 result.hostname = DnsName.from(hostname);
250 result.port = port;
251 result.directTls = useDirectTls(port);
252 result.authenticated = true;
253 return Collections.singletonList(result);
254 }
255
256 public static void checkDomain(final Jid jid) {
257 DnsName.from(jid.getDomain());
258 }
259
260 public static boolean invalidHostname(final String hostname) {
261 try {
262 DnsName.from(hostname);
263 return false;
264 } catch (final InvalidDnsNameException | IllegalArgumentException e) {
265 return true;
266 }
267 }
268
269 public static void clearCache() {
270 final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
271 final DnsCache dnsCache = client.getCache();
272 if (dnsCache instanceof LruCache) {
273 Log.d(Config.LOGTAG,"clearing DNS cache");
274 ((LruCache) dnsCache).clear();
275 }
276
277 final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
278 final DnsCache dnsCacheSec = clientSec.getCache();
279 if (dnsCacheSec instanceof LruCache) {
280 Log.d(Config.LOGTAG,"clearing DNSSEC cache");
281 ((LruCache) dnsCacheSec).clear();
282 }
283 }
284
285 public static boolean useDirectTls(final int port) {
286 return port == 443 || port == 5223;
287 }
288
289 public static List<Result> resolve(final String domain) {
290 final List<Result> ipResults = fromIpAddress(domain);
291 if (!ipResults.isEmpty()) {
292 return ipResults;
293 }
294
295 final var startTls = resolveSrvAsFuture(domain, false);
296 final var directTls = resolveSrvAsFuture(domain, true);
297
298 final var combined = merge(ImmutableList.of(startTls, directTls));
299
300 final var combinedWithFallback =
301 Futures.transformAsync(
302 combined,
303 results -> {
304 if (results.isEmpty()) {
305 return resolveNoSrvAsFuture(DnsName.from(domain), true);
306 } else {
307 return Futures.immediateFuture(results);
308 }
309 },
310 MoreExecutors.directExecutor());
311 final var orderedFuture =
312 Futures.transform(
313 combinedWithFallback,
314 all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
315 MoreExecutors.directExecutor());
316 try {
317 final var ordered = orderedFuture.get();
318 Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
319 return ordered;
320 } catch (final ExecutionException e) {
321 Log.d(Config.LOGTAG, "error resolving DNS", e);
322 return Collections.emptyList();
323 } catch (final InterruptedException e) {
324 Log.d(Config.LOGTAG, "DNS resolution interrupted");
325 return Collections.emptyList();
326 }
327 }
328
329 private static List<Result> fromIpAddress(final String domain) {
330 if (IP.matches(domain)) {
331 final InetAddress inetAddress;
332 try {
333 inetAddress = InetAddress.getByName(domain);
334 } catch (final UnknownHostException e) {
335 return Collections.emptyList();
336 }
337 final Result result = new Result();
338 result.ip = inetAddress;
339 result.port = DEFAULT_PORT_XMPP;
340 return Collections.singletonList(result);
341 } else {
342 return Collections.emptyList();
343 }
344 }
345
346 private static ListenableFuture<List<Result>> resolveSrvAsFuture(
347 final String domain, final boolean directTls) {
348 final DnsName dnsName =
349 DnsName.from(
350 (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
351 final var resultFuture = resolveAsFuture(dnsName, SRV.class);
352 return Futures.transformAsync(
353 resultFuture,
354 result -> resolveIpsAsFuture(result, directTls),
355 MoreExecutors.directExecutor());
356 }
357
358 @NonNull
359 private static ListenableFuture<List<Result>> resolveIpsAsFuture(
360 final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
361 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
362 new ImmutableList.Builder<>();
363 for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
364 if (record.target.length() == 0 && record.priority == 0) {
365 continue;
366 }
367 final var ipv4sRaw =
368 resolveIpsAsFuture(
369 record, A.class, srvResolverResult.isAuthenticData(), directTls);
370 final var ipv4s =
371 Futures.transform(
372 ipv4sRaw,
373 results -> {
374 if (results.isEmpty()) {
375 final Result resolverResult =
376 Result.fromRecord(record, directTls);
377 resolverResult.authenticated =
378 srvResolverResult.isAuthenticData();
379 return Collections.singletonList(resolverResult);
380 } else {
381 return results;
382 }
383 },
384 MoreExecutors.directExecutor());
385 final var ipv6s =
386 resolveIpsAsFuture(
387 record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
388 futuresBuilder.add(ipv4s);
389 futuresBuilder.add(ipv6s);
390 }
391 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
392 return merge(futures);
393 }
394
395 private static ListenableFuture<List<Result>> merge(
396 final Collection<ListenableFuture<List<Result>>> futures) {
397 return Futures.transform(
398 Futures.successfulAsList(futures),
399 lists -> {
400 final var builder = new ImmutableList.Builder<Result>();
401 for (final Collection<Result> list : lists) {
402 if (list == null) {
403 continue;
404 }
405 builder.addAll(list);
406 }
407 return builder.build();
408 },
409 MoreExecutors.directExecutor());
410 }
411
412 private static <D extends InternetAddressRR<?>>
413 ListenableFuture<List<Result>> resolveIpsAsFuture(
414 final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
415 final var resultFuture = resolveAsFuture(srv.target, type);
416 return Futures.transform(
417 resultFuture,
418 result -> {
419 final var builder = new ImmutableList.Builder<Result>();
420 for (D record : result.getAnswersOrEmptySet()) {
421 Result resolverResult = Result.fromRecord(srv, directTls);
422 resolverResult.authenticated =
423 result.isAuthenticData()
424 && authenticated; // TODO technically it does not matter if
425 // the IP
426 // was authenticated
427 resolverResult.ip = record.getInetAddress();
428 builder.add(resolverResult);
429 }
430 return builder.build();
431 },
432 MoreExecutors.directExecutor());
433 }
434
435 private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
436 final DnsName dnsName, boolean cName) {
437 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
438 new ImmutableList.Builder<>();
439 ListenableFuture<List<Result>> aRecordResults =
440 Futures.transform(
441 resolveAsFuture(dnsName, A.class),
442 result ->
443 Lists.transform(
444 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
445 a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())),
446 MoreExecutors.directExecutor());
447 futuresBuilder.add(aRecordResults);
448 ListenableFuture<List<Result>> aaaaRecordResults =
449 Futures.transform(
450 resolveAsFuture(dnsName, AAAA.class),
451 result ->
452 Lists.transform(
453 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
454 aaaa ->
455 Result.createDefault(
456 dnsName, aaaa.getInetAddress(), result.isAuthenticData())),
457 MoreExecutors.directExecutor());
458 futuresBuilder.add(aaaaRecordResults);
459 if (cName) {
460 ListenableFuture<List<Result>> cNameRecordResults =
461 Futures.transformAsync(
462 resolveAsFuture(dnsName, CNAME.class),
463 result -> {
464 Collection<ListenableFuture<List<Result>>> test =
465 Lists.transform(
466 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
467 cname -> resolveNoSrvAsFuture(cname.target, false));
468 return merge(test);
469 },
470 MoreExecutors.directExecutor());
471 futuresBuilder.add(cNameRecordResults);
472 }
473 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
474 final var noSrvFallbacks = merge(futures);
475 return Futures.transform(
476 noSrvFallbacks,
477 results -> {
478 if (results.isEmpty()) {
479 return Collections.singletonList(Result.createDefault(dnsName));
480 } else {
481 return results;
482 }
483 },
484 MoreExecutors.directExecutor());
485 }
486
487 private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
488 final DnsName dnsName, final Class<D> type) {
489 return Futures.submit(
490 () -> {
491 final Question question = new Question(dnsName, Record.TYPE.getType(type));
492 if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
493 for (int i = 0; i < 3; i++) {
494 try {
495 ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
496 if (result.wasSuccessful() && !result.isAuthenticData()) {
497 Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
498 }
499 return result;
500 } catch (DnssecValidationFailedException e) {
501 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
502 // Try again, may be transient DNSSEC failure https://github.com/MiniDNS/minidns/issues/132
503 } catch (Throwable throwable) {
504 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
505 break;
506 }
507 }
508 }
509 return ResolverApi.INSTANCE.resolve(question);
510 },
511 DNS_QUERY_EXECUTOR);
512 }
513
514 public static class Result {
515 public static final String DOMAIN = "domain";
516 public static final String IP = "ip";
517 public static final String HOSTNAME = "hostname";
518 public static final String PORT = "port";
519 public static final String PRIORITY = "priority";
520 public static final String DIRECT_TLS = "directTls";
521 public static final String AUTHENTICATED = "authenticated";
522 private InetAddress ip;
523 private DnsName hostname;
524 private int port = DEFAULT_PORT_XMPP;
525 private boolean directTls = false;
526 private boolean authenticated = false;
527 private int priority;
528
529 static Result fromRecord(final SRV srv, final boolean directTls) {
530 final Result result = new Result();
531 result.port = srv.port;
532 result.hostname = srv.target;
533 result.directTls = directTls;
534 result.priority = srv.priority;
535 return result;
536 }
537
538 static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
539 Result result = new Result();
540 result.port = DEFAULT_PORT_XMPP;
541 result.hostname = hostname;
542 result.ip = ip;
543 result.authenticated = authenticated;
544 return result;
545 }
546
547 static Result createDefault(final DnsName hostname) {
548 return createDefault(hostname, null, false);
549 }
550
551 public static Result fromCursor(final Cursor cursor) {
552 final Result result = new Result();
553 try {
554 result.ip =
555 InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
556 } catch (final UnknownHostException e) {
557 result.ip = null;
558 }
559 final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
560 result.hostname = hostname == null ? null : DnsName.from(hostname);
561 result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
562 result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
563 result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
564 result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
565 return result;
566 }
567
568 @Override
569 public boolean equals(Object o) {
570 if (this == o) return true;
571 if (o == null || getClass() != o.getClass()) return false;
572 Result result = (Result) o;
573 return port == result.port
574 && directTls == result.directTls
575 && authenticated == result.authenticated
576 && priority == result.priority
577 && Objects.equal(ip, result.ip)
578 && Objects.equal(hostname, result.hostname);
579 }
580
581 @Override
582 public int hashCode() {
583 return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
584 }
585
586 public InetAddress getIp() {
587 return ip;
588 }
589
590 public int getPort() {
591 return port;
592 }
593
594 public DnsName getHostname() {
595 return hostname;
596 }
597
598 public boolean isDirectTls() {
599 return directTls;
600 }
601
602 public boolean isAuthenticated() {
603 return authenticated;
604 }
605
606 @Override
607 @NonNull
608 public String toString() {
609 return MoreObjects.toStringHelper(this)
610 .add("ip", ip)
611 .add("hostname", hostname)
612 .add("port", port)
613 .add("directTls", directTls)
614 .add("authenticated", authenticated)
615 .add("priority", priority)
616 .toString();
617 }
618
619 public ContentValues toContentValues() {
620 final ContentValues contentValues = new ContentValues();
621 contentValues.put(IP, ip == null ? null : ip.getAddress());
622 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
623 contentValues.put(PORT, port);
624 contentValues.put(PRIORITY, priority);
625 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
626 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
627 return contentValues;
628 }
629
630 public Result seeOtherHost(final String seeOtherHost) {
631 final String hostname = seeOtherHost.trim();
632 if (hostname.isEmpty()) {
633 return null;
634 }
635 final Result result = new Result();
636 result.directTls = this.directTls;
637 final int portSegmentStart = hostname.lastIndexOf(':');
638 if (hostname.charAt(hostname.length() - 1) != ']'
639 && portSegmentStart >= 0
640 && hostname.length() >= portSegmentStart + 1) {
641 final String hostPart = hostname.substring(0, portSegmentStart);
642 final String portPart = hostname.substring(portSegmentStart + 1);
643 final Integer port = Ints.tryParse(portPart);
644 if (port == null || Strings.isNullOrEmpty(hostPart)) {
645 return null;
646 }
647 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
648 result.port = port;
649 if (InetAddresses.isInetAddress(host)) {
650 final InetAddress inetAddress;
651 try {
652 inetAddress = InetAddresses.forString(host);
653 } catch (final IllegalArgumentException e) {
654 return null;
655 }
656 result.ip = inetAddress;
657 } else {
658 if (hostPart.trim().isEmpty()) {
659 return null;
660 }
661 try {
662 result.hostname = DnsName.from(hostPart.trim());
663 } catch (final Exception e) {
664 return null;
665 }
666 }
667 } else {
668 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
669 if (InetAddresses.isInetAddress(host)) {
670 final InetAddress inetAddress;
671 try {
672 inetAddress = InetAddresses.forString(host);
673 } catch (final IllegalArgumentException e) {
674 return null;
675 }
676 result.ip = inetAddress;
677 } else {
678 try {
679 result.hostname = DnsName.from(hostname);
680 } catch (final Exception e) {
681 return null;
682 }
683 }
684 result.port = port;
685 }
686 return result;
687 }
688 }
689}