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 continue; 227 } 228 } 229 230 List<MiniDnsFuture<DnsQueryResult, IOException>> futures = new ArrayList<>(dnsServerAddresses.size()); 231 // "Main" loop. 232 for (InetAddress dns : dnsServerAddresses) { 233 MiniDnsFuture<DnsQueryResult, IOException> f = queryAsync(q, dns); 234 futures.add(f); 235 } 236 237 return MiniDnsFuture.anySuccessfulOf(futures); 238 } 239 240 /** 241 * Retrieve a list of currently configured DNS servers IP addresses. This method does verify that only IP addresses are returned and 242 * nothing else (e.g. DNS names). 243 * <p> 244 * The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s. 245 * </p> 246 * 247 * @return A list of DNS server IP addresses configured for this system. 248 */ 249 public static List<String> findDNS() { 250 List<String> res = null; 251 final Level TRACE_LOG_LEVEL = Level.FINE; 252 for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) { 253 try { 254 res = mechanism.getDnsServerAddresses(); 255 } catch (SecurityException exception) { 256 LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception); 257 } 258 if (res == null) { 259 LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server"); 260 continue; 261 } 262 263 if (LOGGER.isLoggable(TRACE_LOG_LEVEL)) { 264 // TODO: Use String.join() once MiniDNS is Android API 26 (or higher). 265 StringBuilder sb = new StringBuilder(); 266 for (Iterator<String> it = res.iterator(); it.hasNext();) { 267 String s = it.next(); 268 sb.append(s); 269 if (it.hasNext()) { 270 sb.append(", "); 271 } 272 } 273 String dnsServers = sb.toString(); 274 LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism {0} returned the following DNS servers: {1}", 275 new Object[] { mechanism.getName(), dnsServers }); 276 } 277 278 assert !res.isEmpty(); 279 280 // We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial 281 // though, because the list returned by the server mechanism is rather short. 282 283 // Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else, 284 // especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using 285 // getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside 286 // of its DNSSEC guarantees. 287 Iterator<String> it = res.iterator(); 288 while (it.hasNext()) { 289 String potentialDnsServer = it.next(); 290 if (!InetAddressUtil.isIpAddress(potentialDnsServer)) { 291 LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() 292 + "' returned an invalid non-IP address result: '" + potentialDnsServer + "'"); 293 it.remove(); 294 } else if (blacklistedDnsServers.contains(potentialDnsServer)) { 295 LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName() 296 + "' returned a blacklisted result: '" + potentialDnsServer + "'"); 297 it.remove(); 298 } 299 } 300 301 if (!res.isEmpty()) { 302 break; 303 } 304 305 LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() 306 + "' returned not a single valid IP address after sanitazion"); 307 res = null; 308 } 309 310 return res; 311 } 312 313 /** 314 * Retrieve a list of currently configured DNS server addresses. 315 * <p> 316 * Note that unlike {@link #findDNS()}, the list returned by this method 317 * will take the IP version setting into account, and order the list by the 318 * preferred address types (IPv4/v6). The returned list is modifiable. 319 * </p> 320 * 321 * @return A list of DNS server addresses. 322 * @see #findDNS() 323 */ 324 public static List<InetAddress> findDnsAddresses() { 325 // The findDNS() method contract guarantees that only IP addresses will be returned. 326 List<String> res = findDNS(); 327 328 if (res == null) { 329 return new ArrayList<>(); 330 } 331 332 final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING; 333 334 List<Inet4Address> ipv4DnsServer = null; 335 List<Inet6Address> ipv6DnsServer = null; 336 if (setting.v4) { 337 ipv4DnsServer = new ArrayList<>(res.size()); 338 } 339 if (setting.v6) { 340 ipv6DnsServer = new ArrayList<>(res.size()); 341 } 342 343 int validServerAddresses = 0; 344 for (String dnsServerString : res) { 345 // The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings 346 // representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being 347 // involved. Something we want to avoid. 348 assert InetAddressUtil.isIpAddress(dnsServerString); 349 350 InetAddress dnsServerAddress; 351 try { 352 dnsServerAddress = InetAddress.getByName(dnsServerString); 353 } catch (UnknownHostException e) { 354 LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e); 355 continue; 356 } 357 if (dnsServerAddress instanceof Inet4Address) { 358 if (!setting.v4) { 359 continue; 360 } 361 Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress; 362 ipv4DnsServer.add(ipv4DnsServerAddress); 363 } else if (dnsServerAddress instanceof Inet6Address) { 364 if (!setting.v6) { 365 continue; 366 } 367 Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress; 368 ipv6DnsServer.add(ipv6DnsServerAddress); 369 } else { 370 throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address"); 371 } 372 373 validServerAddresses++; 374 } 375 376 List<InetAddress> dnsServers = new ArrayList<>(validServerAddresses); 377 378 switch (setting) { 379 case v4v6: 380 dnsServers.addAll(ipv4DnsServer); 381 dnsServers.addAll(ipv6DnsServer); 382 break; 383 case v6v4: 384 dnsServers.addAll(ipv6DnsServer); 385 dnsServers.addAll(ipv4DnsServer); 386 break; 387 case v4only: 388 dnsServers.addAll(ipv4DnsServer); 389 break; 390 case v6only: 391 dnsServers.addAll(ipv6DnsServer); 392 break; 393 } 394 return dnsServers; 395 } 396 397 public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { 398 if (!dnsServerLookup.isAvailable()) { 399 LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available."); 400 return; 401 } 402 synchronized (LOOKUP_MECHANISMS) { 403 // We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace 404 // LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see 405 // http://stackoverflow.com/a/34827492/194894 406 // TODO: Remove that workaround once MiniDNS is Java 8 only. 407 ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1); 408 tempList.addAll(LOOKUP_MECHANISMS); 409 tempList.add(dnsServerLookup); 410 411 // Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7. 412 Collections.sort(tempList); 413 414 LOOKUP_MECHANISMS.clear(); 415 LOOKUP_MECHANISMS.addAll(tempList); 416 } 417 } 418 419 public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { 420 synchronized (LOOKUP_MECHANISMS) { 421 return LOOKUP_MECHANISMS.remove(dnsServerLookup); 422 } 423 } 424 425 public static boolean addBlacklistedDnsServer(String dnsServer) { 426 return blacklistedDnsServers.add(dnsServer); 427 } 428 429 public static boolean removeBlacklistedDnsServer(String dnsServer) { 430 return blacklistedDnsServers.remove(dnsServer); 431 } 432 433 public boolean isAskForDnssec() { 434 return askForDnssec; 435 } 436 437 public void setAskForDnssec(boolean askForDnssec) { 438 this.askForDnssec = askForDnssec; 439 } 440 441 public boolean isDisableResultFilter() { 442 return disableResultFilter; 443 } 444 445 public void setDisableResultFilter(boolean disableResultFilter) { 446 this.disableResultFilter = disableResultFilter; 447 } 448 449 public boolean isUseHardcodedDnsServersEnabled() { 450 return useHardcodedDnsServers; 451 } 452 453 public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) { 454 this.useHardcodedDnsServers = useHardcodedDnsServers; 455 } 456 457 public InetAddress getRandomHardcodedIpv4DnsServer() { 458 return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom); 459 } 460 461 public InetAddress getRandomHarcodedIpv6DnsServer() { 462 return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom); 463 } 464 465 private static Question getReverseIpLookupQuestionFor(DnsName dnsName) { 466 return new Question(dnsName, TYPE.PTR); 467 } 468 469 public static Question getReverseIpLookupQuestionFor(Inet4Address inet4Address) { 470 DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet4Address); 471 DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IN_ADDR_ARPA); 472 return getReverseIpLookupQuestionFor(dnsName); 473 } 474 475 public static Question getReverseIpLookupQuestionFor(Inet6Address inet6Address) { 476 DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet6Address); 477 DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IP6_ARPA); 478 return getReverseIpLookupQuestionFor(dnsName); 479 } 480 481 public static Question getReverseIpLookupQuestionFor(InetAddress inetAddress) { 482 if (inetAddress instanceof Inet4Address) { 483 return getReverseIpLookupQuestionFor((Inet4Address) inetAddress); 484 } else if (inetAddress instanceof Inet6Address) { 485 return getReverseIpLookupQuestionFor((Inet6Address) inetAddress); 486 } else { 487 throw new IllegalArgumentException("The provided inetAddress '" + inetAddress 488 + "' is neither of type Inet4Address nor Inet6Address"); 489 } 490 } 491 492}