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