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 final var start = System.currentTimeMillis();
490 return Futures.submit(
491 () -> {
492 final Question question = new Question(dnsName, Record.TYPE.getType(type));
493 if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
494 for (int i = 0; i < 5; i++) {
495 if (System.currentTimeMillis() - start > 5000) break;
496 try {
497 ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
498 if (result.wasSuccessful() && !result.isAuthenticData()) {
499 Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
500 }
501 return result;
502 } catch (DnssecValidationFailedException e) {
503 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
504 // Try again, may be transient DNSSEC failure https://github.com/MiniDNS/minidns/issues/132
505 } catch (Throwable throwable) {
506 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
507 break;
508 }
509 }
510 }
511 return ResolverApi.INSTANCE.resolve(question);
512 },
513 DNS_QUERY_EXECUTOR);
514 }
515
516 public static class Result {
517 public static final String DOMAIN = "domain";
518 public static final String IP = "ip";
519 public static final String HOSTNAME = "hostname";
520 public static final String PORT = "port";
521 public static final String PRIORITY = "priority";
522 public static final String DIRECT_TLS = "directTls";
523 public static final String AUTHENTICATED = "authenticated";
524 private InetAddress ip;
525 private DnsName hostname;
526 private int port = DEFAULT_PORT_XMPP;
527 private boolean directTls = false;
528 private boolean authenticated = false;
529 private int priority;
530
531 static Result fromRecord(final SRV srv, final boolean directTls) {
532 final Result result = new Result();
533 result.port = srv.port;
534 result.hostname = srv.target;
535 result.directTls = directTls;
536 result.priority = srv.priority;
537 return result;
538 }
539
540 static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
541 Result result = new Result();
542 result.port = DEFAULT_PORT_XMPP;
543 result.hostname = hostname;
544 result.ip = ip;
545 result.authenticated = authenticated;
546 return result;
547 }
548
549 static Result createDefault(final DnsName hostname) {
550 return createDefault(hostname, null, false);
551 }
552
553 public static Result fromCursor(final Cursor cursor) {
554 final Result result = new Result();
555 try {
556 result.ip =
557 InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
558 } catch (final UnknownHostException e) {
559 result.ip = null;
560 }
561 final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
562 result.hostname = hostname == null ? null : DnsName.from(hostname);
563 result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
564 result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
565 result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
566 result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
567 return result;
568 }
569
570 @Override
571 public boolean equals(Object o) {
572 if (this == o) return true;
573 if (o == null || getClass() != o.getClass()) return false;
574 Result result = (Result) o;
575 return port == result.port
576 && directTls == result.directTls
577 && authenticated == result.authenticated
578 && priority == result.priority
579 && Objects.equal(ip, result.ip)
580 && Objects.equal(hostname, result.hostname);
581 }
582
583 @Override
584 public int hashCode() {
585 return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
586 }
587
588 public InetAddress getIp() {
589 return ip;
590 }
591
592 public int getPort() {
593 return port;
594 }
595
596 public DnsName getHostname() {
597 return hostname;
598 }
599
600 public boolean isDirectTls() {
601 return directTls;
602 }
603
604 public boolean isAuthenticated() {
605 return authenticated;
606 }
607
608 @Override
609 @NonNull
610 public String toString() {
611 return MoreObjects.toStringHelper(this)
612 .add("ip", ip)
613 .add("hostname", hostname)
614 .add("port", port)
615 .add("directTls", directTls)
616 .add("authenticated", authenticated)
617 .add("priority", priority)
618 .toString();
619 }
620
621 public ContentValues toContentValues() {
622 final ContentValues contentValues = new ContentValues();
623 contentValues.put(IP, ip == null ? null : ip.getAddress());
624 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
625 contentValues.put(PORT, port);
626 contentValues.put(PRIORITY, priority);
627 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
628 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
629 return contentValues;
630 }
631
632 public Result seeOtherHost(final String seeOtherHost) {
633 final String hostname = seeOtherHost.trim();
634 if (hostname.isEmpty()) {
635 return null;
636 }
637 final Result result = new Result();
638 result.directTls = this.directTls;
639 final int portSegmentStart = hostname.lastIndexOf(':');
640 if (hostname.charAt(hostname.length() - 1) != ']'
641 && portSegmentStart >= 0
642 && hostname.length() >= portSegmentStart + 1) {
643 final String hostPart = hostname.substring(0, portSegmentStart);
644 final String portPart = hostname.substring(portSegmentStart + 1);
645 final Integer port = Ints.tryParse(portPart);
646 if (port == null || Strings.isNullOrEmpty(hostPart)) {
647 return null;
648 }
649 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
650 result.port = port;
651 if (InetAddresses.isInetAddress(host)) {
652 final InetAddress inetAddress;
653 try {
654 inetAddress = InetAddresses.forString(host);
655 } catch (final IllegalArgumentException e) {
656 return null;
657 }
658 result.ip = inetAddress;
659 } else {
660 if (hostPart.trim().isEmpty()) {
661 return null;
662 }
663 try {
664 result.hostname = DnsName.from(hostPart.trim());
665 } catch (final Exception e) {
666 return null;
667 }
668 }
669 } else {
670 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
671 if (InetAddresses.isInetAddress(host)) {
672 final InetAddress inetAddress;
673 try {
674 inetAddress = InetAddresses.forString(host);
675 } catch (final IllegalArgumentException e) {
676 return null;
677 }
678 result.ip = inetAddress;
679 } else {
680 try {
681 result.hostname = DnsName.from(hostname);
682 } catch (final Exception e) {
683 return null;
684 }
685 }
686 result.port = port;
687 }
688 return result;
689 }
690 }
691}