001/* 002 * Copyright 2015-2024 the original author or authors 003 * 004 * This software is licensed under the Apache License, Version 2.0, 005 * the GNU Lesser General Public License version 2 or later ("LGPL") 006 * and the WTFPL. 007 * You may choose either license to govern your use of this software only 008 * upon the condition that you accept all of the terms of either 009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. 010 */ 011package org.minidns; 012 013import org.minidns.MiniDnsException.ErrorResponseException; 014import org.minidns.MiniDnsException.NoQueryPossibleException; 015import org.minidns.dnsmessage.DnsMessage; 016import org.minidns.dnsmessage.Question; 017import org.minidns.dnsname.DnsName; 018import org.minidns.dnsqueryresult.DnsQueryResult; 019import org.minidns.dnsserverlookup.AndroidUsingExec; 020import org.minidns.dnsserverlookup.AndroidUsingReflection; 021import org.minidns.dnsserverlookup.DnsServerLookupMechanism; 022import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf; 023import org.minidns.record.Record.TYPE; 024import org.minidns.util.CollectionsUtil; 025import org.minidns.util.InetAddressUtil; 026import org.minidns.util.MultipleIoException; 027 028import java.io.IOException; 029import java.net.Inet4Address; 030import java.net.Inet6Address; 031import java.net.InetAddress; 032import java.net.UnknownHostException; 033import java.util.ArrayList; 034import java.util.Collections; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Set; 038import java.util.concurrent.ConcurrentHashMap; 039import java.util.concurrent.CopyOnWriteArrayList; 040import java.util.concurrent.CopyOnWriteArraySet; 041import java.util.logging.Level; 042 043/** 044 * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. 045 * This circumvents the missing javax.naming package on android. 046 */ 047public class DnsClient extends AbstractDnsClient { 048 049 static final List<DnsServerLookupMechanism> LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>(); 050 051 static final Set<Inet4Address> STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>(); 052 static final Set<Inet6Address> STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>(); 053 054 static { 055 addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE); 056 addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE); 057 addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE); 058 059 try { 060 Inet4Address googleV4Dns = InetAddressUtil.ipv4From("8.8.8.8"); 061 STATIC_IPV4_DNS_SERVERS.add(googleV4Dns); 062 } catch (IllegalArgumentException e) { 063 LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e); 064 } 065 066 try { 067 Inet6Address googleV6Dns = InetAddressUtil.ipv6From("[2001:4860:4860::8888]"); 068 STATIC_IPV6_DNS_SERVERS.add(googleV6Dns); 069 } catch (IllegalArgumentException e) { 070 LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e); 071 } 072 } 073 074 private static final Set<String> blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(4)); 075 076 private final Set<InetAddress> nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>(4)); 077 078 private boolean askForDnssec = false; 079 private boolean disableResultFilter = false; 080 081 private boolean useHardcodedDnsServers = true; 082 083 /** 084 * Create a new DNS client using the global default cache. 085 */ 086 public DnsClient() { 087 super(); 088 } 089 090 public DnsClient(DnsCache dnsCache) { 091 super(dnsCache); 092 } 093 094 @Override 095 protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { 096 message.setRecursionDesired(true); 097 message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(askForDnssec); 098 return message; 099 } 100 101 private List<InetAddress> getServerAddresses() { 102 List<InetAddress> dnsServerAddresses = findDnsAddresses(); 103 104 if (useHardcodedDnsServers) { 105 InetAddress primaryHardcodedDnsServer, secondaryHardcodedDnsServer = null; 106 switch (ipVersionSetting) { 107 case v4v6: 108 primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); 109 secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); 110 break; 111 case v6v4: 112 primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); 113 secondaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); 114 break; 115 case v4only: 116 primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); 117 break; 118 case v6only: 119 primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); 120 break; 121 default: 122 throw new AssertionError("Unknown ipVersionSetting: " + ipVersionSetting); 123 } 124 125 dnsServerAddresses.add(primaryHardcodedDnsServer); 126 if (secondaryHardcodedDnsServer != null) { 127 dnsServerAddresses.add(secondaryHardcodedDnsServer); 128 } 129 } 130 131 return dnsServerAddresses; 132 } 133 134 @Override 135 public DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException { 136 DnsMessage q = newQuestion(queryBuilder).build(); 137 // While this query method does in fact re-use query(Question, String) 138 // we still do a cache lookup here in order to avoid unnecessary 139 // findDNS()calls, which are expensive on Android. Note that we do not 140 // put the results back into the Cache, as this is already done by 141 // query(Question, String). 142 DnsQueryResult dnsQueryResult = (cache == null) ? null : cache.get(q); 143 if (dnsQueryResult != null) { 144 return dnsQueryResult; 145 } 146 147 List<InetAddress> dnsServerAddresses = getServerAddresses(); 148 149 List<IOException> ioExceptions = new ArrayList<>(dnsServerAddresses.size()); 150 for (InetAddress dns : dnsServerAddresses) { 151 if (nonRaServers.contains(dns)) { 152 LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\""); 153 continue; 154 } 155 156 try { 157 dnsQueryResult = query(q, dns); 158 } catch (IOException ioe) { 159 ioExceptions.add(ioe); 160 continue; 161 } 162 163 DnsMessage responseMessage = dnsQueryResult.response; 164 if (!responseMessage.recursionAvailable) { 165 boolean newRaServer = nonRaServers.add(dns); 166 if (newRaServer) { 167 LOGGER.warning("The DNS server " + dns 168 + " returned a response without the \"recursion available\" (RA) flag set. This likely indicates a misconfiguration because the server is not suitable for DNS resolution"); 169 } 170 continue; 171 } 172 173 if (disableResultFilter) { 174 return dnsQueryResult; 175 } 176 177 switch (responseMessage.responseCode) { 178 case NO_ERROR: 179 case NX_DOMAIN: 180 break; 181 default: 182 String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: " 183 + responseMessage.responseCode + '.'; 184 if (!LOGGER.isLoggable(Level.FINE)) { 185 // Only append the responseMessage is log level is not fine. If it is fine or higher, the 186 // response has already been logged. 187 warning += "\n" + responseMessage; 188 } 189 LOGGER.warning(warning); 190 191 ErrorResponseException exception = new ErrorResponseException(q, dnsQueryResult); 192 ioExceptions.add(exception); 193 continue; 194 } 195 196 return dnsQueryResult; 197 } 198 MultipleIoException.throwIfRequired(ioExceptions); 199 200 // TODO: Shall we add the attempted DNS servers to the exception? 201 throw new NoQueryPossibleException(q); 202 } 203 204 @Override 205 protected MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage.Builder queryBuilder) { 206 DnsMessage q = newQuestion(queryBuilder).build(); 207 // While this query method does in fact re-use query(Question, String) 208 // we still do a cache lookup here in order to avoid unnecessary 209 // findDNS()calls, which are expensive on Android. Note that we do not 210 // put the results back into the Cache, as this is already done by 211 // query(Question, String). 212 DnsQueryResult responseMessage = (cache == null) ? null : cache.get(q); 213 if (responseMessage != null) { 214 return MiniDnsFuture.from(responseMessage); 215 } 216 217 final List<InetAddress> dnsServerAddresses = getServerAddresses(); 218 219 // Filter loop. 220 Iterator<InetAddress> it = dnsServerAddresses.iterator(); 221 while (it.hasNext()) { 222 InetAddress dns = it.next(); 223 if (nonRaServers.contains(dns)) { 224 it.remove(); 225 LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\""); 226 } 227 } 228 229 List<MiniDnsFuture<DnsQueryResult, IOException>> futures = new ArrayList<>(dnsServerAddresses.size()); 230 // "Main" loop. 231 for (InetAddress dns : dnsServerAddresses) { 232 MiniDnsFuture<DnsQueryResult, IOException> f = queryAsync(q, dns); 233 futures.add(f); 234 } 235 236 return MiniDnsFuture.anySuccessfulOf(futures); 237 } 238 239 /** 240 * Retrieve a list of currently configured DNS servers IP addresses. This method does verify that only IP addresses are returned and 241 * nothing else (e.g. DNS names). 242 * <p> 243 * The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s. 244 * </p> 245 * 246 * @return A list of DNS server IP addresses configured for this system. 247 */ 248 public static List<String> findDNS() { 249 List<String> res = null; 250 final Level TRACE_LOG_LEVEL = Level.FINE; 251 for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) { 252 try { 253 res = mechanism.getDnsServerAddresses(); 254 } catch (SecurityException exception) { 255 LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception); 256 } 257 if (res == null) { 258 LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server"); 259 continue; 260 } 261 262 if (LOGGER.isLoggable(TRACE_LOG_LEVEL)) { 263 // TODO: Use String.join() once MiniDNS is Android API 26 (or higher). 264 StringBuilder sb = new StringBuilder(); 265 for (Iterator<String> it = res.iterator(); it.hasNext();) { 266 String s = it.next(); 267 sb.append(s); 268 if (it.hasNext()) { 269 sb.append(", "); 270 } 271 } 272 String dnsServers = sb.toString(); 273 LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism {0} returned the following DNS servers: {1}", 274 new Object[] { mechanism.getName(), dnsServers }); 275 } 276 277 assert !res.isEmpty(); 278 279 // We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial 280 // though, because the list returned by the server mechanism is rather short. 281 282 // Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else, 283 // especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using 284 // getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside 285 // of its DNSSEC guarantees. 286 Iterator<String> it = res.iterator(); 287 while (it.hasNext()) { 288 String potentialDnsServer = it.next(); 289 if (!InetAddressUtil.isIpAddress(potentialDnsServer)) { 290 LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() 291 + "' returned an invalid non-IP address result: '" + potentialDnsServer + "'"); 292 it.remove(); 293 } else if (blacklistedDnsServers.contains(potentialDnsServer)) { 294 LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName() 295 + "' returned a blacklisted result: '" + potentialDnsServer + "'"); 296 it.remove(); 297 } 298 } 299 300 if (!res.isEmpty()) { 301 break; 302 } 303 304 LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() 305 + "' returned not a single valid IP address after sanitazion"); 306 res = null; 307 } 308 309 return res; 310 } 311 312 /** 313 * Retrieve a list of currently configured DNS server addresses. 314 * <p> 315 * Note that unlike {@link #findDNS()}, the list returned by this method 316 * will take the IP version setting into account, and order the list by the 317 * preferred address types (IPv4/v6). The returned list is modifiable. 318 * </p> 319 * 320 * @return A list of DNS server addresses. 321 * @see #findDNS() 322 */ 323 public static List<InetAddress> findDnsAddresses() { 324 // The findDNS() method contract guarantees that only IP addresses will be returned. 325 List<String> res = findDNS(); 326 327 if (res == null) { 328 return new ArrayList<>(); 329 } 330 331 final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING; 332 333 List<Inet4Address> ipv4DnsServer = null; 334 List<Inet6Address> ipv6DnsServer = null; 335 if (setting.v4) { 336 ipv4DnsServer = new ArrayList<>(res.size()); 337 } 338 if (setting.v6) { 339 ipv6DnsServer = new ArrayList<>(res.size()); 340 } 341 342 int validServerAddresses = 0; 343 for (String dnsServerString : res) { 344 // The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings 345 // representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being 346 // involved. Something we want to avoid. 347 assert InetAddressUtil.isIpAddress(dnsServerString); 348 349 InetAddress dnsServerAddress; 350 try { 351 dnsServerAddress = InetAddress.getByName(dnsServerString); 352 } catch (UnknownHostException e) { 353 LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e); 354 continue; 355 } 356 if (dnsServerAddress instanceof Inet4Address) { 357 if (!setting.v4) { 358 continue; 359 } 360 Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress; 361 ipv4DnsServer.add(ipv4DnsServerAddress); 362 } else if (dnsServerAddress instanceof Inet6Address) { 363 if (!setting.v6) { 364 continue; 365 } 366 Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress; 367 ipv6DnsServer.add(ipv6DnsServerAddress); 368 } else { 369 throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address"); 370 } 371 372 validServerAddresses++; 373 } 374 375 List<InetAddress> dnsServers = new ArrayList<>(validServerAddresses); 376 377 switch (setting) { 378 case v4v6: 379 dnsServers.addAll(ipv4DnsServer); 380 dnsServers.addAll(ipv6DnsServer); 381 break; 382 case v6v4: 383 dnsServers.addAll(ipv6DnsServer); 384 dnsServers.addAll(ipv4DnsServer); 385 break; 386 case v4only: 387 dnsServers.addAll(ipv4DnsServer); 388 break; 389 case v6only: 390 dnsServers.addAll(ipv6DnsServer); 391 break; 392 } 393 return dnsServers; 394 } 395 396 public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { 397 if (!dnsServerLookup.isAvailable()) { 398 LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available."); 399 return; 400 } 401 synchronized (LOOKUP_MECHANISMS) { 402 // We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace 403 // LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see 404 // http://stackoverflow.com/a/34827492/194894 405 // TODO: Remove that workaround once MiniDNS is Java 8 only. 406 ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1); 407 tempList.addAll(LOOKUP_MECHANISMS); 408 tempList.add(dnsServerLookup); 409 410 // Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7. 411 Collections.sort(tempList); 412 413 LOOKUP_MECHANISMS.clear(); 414 LOOKUP_MECHANISMS.addAll(tempList); 415 } 416 } 417 418 public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { 419 synchronized (LOOKUP_MECHANISMS) { 420 return LOOKUP_MECHANISMS.remove(dnsServerLookup); 421 } 422 } 423 424 public static boolean addBlacklistedDnsServer(String dnsServer) { 425 return blacklistedDnsServers.add(dnsServer); 426 } 427 428 public static boolean removeBlacklistedDnsServer(String dnsServer) { 429 return blacklistedDnsServers.remove(dnsServer); 430 } 431 432 public boolean isAskForDnssec() { 433 return askForDnssec; 434 } 435 436 public void setAskForDnssec(boolean askForDnssec) { 437 this.askForDnssec = askForDnssec; 438 } 439 440 public boolean isDisableResultFilter() { 441 return disableResultFilter; 442 } 443 444 public void setDisableResultFilter(boolean disableResultFilter) { 445 this.disableResultFilter = disableResultFilter; 446 } 447 448 public boolean isUseHardcodedDnsServersEnabled() { 449 return useHardcodedDnsServers; 450 } 451 452 public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) { 453 this.useHardcodedDnsServers = useHardcodedDnsServers; 454 } 455 456 public InetAddress getRandomHardcodedIpv4DnsServer() { 457 return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom); 458 } 459 460 public InetAddress getRandomHarcodedIpv6DnsServer() { 461 return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom); 462 } 463 464 private static Question getReverseIpLookupQuestionFor(DnsName dnsName) { 465 return new Question(dnsName, TYPE.PTR); 466 } 467 468 public static Question getReverseIpLookupQuestionFor(Inet4Address inet4Address) { 469 DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet4Address); 470 DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IN_ADDR_ARPA); 471 return getReverseIpLookupQuestionFor(dnsName); 472 } 473 474 public static Question getReverseIpLookupQuestionFor(Inet6Address inet6Address) { 475 DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet6Address); 476 DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IP6_ARPA); 477 return getReverseIpLookupQuestionFor(dnsName); 478 } 479 480 public static Question getReverseIpLookupQuestionFor(InetAddress inetAddress) { 481 if (inetAddress instanceof Inet4Address) { 482 return getReverseIpLookupQuestionFor((Inet4Address) inetAddress); 483 } else if (inetAddress instanceof Inet6Address) { 484 return getReverseIpLookupQuestionFor((Inet6Address) inetAddress); 485 } else { 486 throw new IllegalArgumentException("The provided inetAddress '" + inetAddress 487 + "' is neither of type Inet4Address nor Inet6Address"); 488 } 489 } 490 491}