1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity; 18 19 import static android.net.DnsResolver.CLASS_IN; 20 21 import static com.android.net.module.util.CollectionUtils.isEmpty; 22 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OFF; 23 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC; 24 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 25 import static com.android.net.module.util.DnsPacket.TYPE_SVCB; 26 27 import android.annotation.IntDef; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.net.DnsResolver; 31 import android.net.LinkProperties; 32 import android.net.Network; 33 import android.net.shared.PrivateDnsConfig; 34 import android.os.CancellationSignal; 35 import android.text.TextUtils; 36 import android.util.Log; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.net.module.util.DnsPacket; 40 import com.android.net.module.util.DnsSvcbPacket; 41 import com.android.net.module.util.SharedLog; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.net.InetAddress; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Collections; 49 import java.util.List; 50 import java.util.concurrent.Executor; 51 52 /** 53 * A class to perform DDR on a given network. 54 * 55 * Caller can use startSvcbLookup() to perform DNS SVCB lookup asynchronously. The result of the 56 * lookup will be passed to callers through the callback onSvcbLookupComplete(). If the result is 57 * stale, the callback won't be invoked. A result becomes stale once there's a new call to 58 * startSvcbLookup(). 59 * 60 * Threading: 61 * 62 * 1. DdrTracker is not thread-safe. All public methods must be executed on the same thread to 63 * guarantee that all DdrTracker members are synchronized. 64 * 2. In DdrTracker constructor, an Executor is provided as the execution thread on which the 65 * callback onSvcbLookupComplete() will be executed. The execution thread must be the same 66 * as the thread mentioned in 1. 67 */ 68 class DdrTracker { 69 private static final String TAG = "DDR"; 70 private static final boolean DBG = true; 71 72 @IntDef(prefix = { "PRIVATE_DNS_MODE_" }, value = { 73 PRIVATE_DNS_MODE_OFF, 74 PRIVATE_DNS_MODE_OPPORTUNISTIC, 75 PRIVATE_DNS_MODE_PROVIDER_HOSTNAME 76 }) 77 @Retention(RetentionPolicy.SOURCE) 78 private @interface PrivateDnsMode {} 79 80 @VisibleForTesting 81 static final String DDR_HOSTNAME = "_dns.resolver.arpa"; 82 83 private static final String ALPN_DOH3 = "h3"; 84 85 interface Callback { 86 /** 87 * Called on a given execution thread `mExecutor` when a SVCB lookup finishes, unless 88 * the lookup result is stale. 89 * The parameter `result` contains the aggregated result that contains both DoH and DoT 90 * information. 91 */ onSvcbLookupComplete(@onNull PrivateDnsConfig result)92 void onSvcbLookupComplete(@NonNull PrivateDnsConfig result); 93 } 94 95 @NonNull 96 private final Network mCleartextDnsNetwork; 97 @NonNull 98 private final DnsResolver mDnsResolver; 99 @NonNull 100 private final Callback mCallback; 101 102 // The execution thread the callback will be executed on. 103 @NonNull 104 private final Executor mExecutor; 105 106 // Stores the DNS information that is synced with current DNS configuration. 107 @NonNull 108 private DnsInfo mDnsInfo; 109 110 // Stores the DoT servers discovered from strict mode hostname resolution. 111 @NonNull 112 private final List<InetAddress> mDotServers; 113 114 // Stores the result of latest SVCB lookup. 115 // It is set to null if the result is invalid, for example, lookup timeout or invalid 116 // SVCB responses. 117 @Nullable 118 private DnsSvcbPacket mLatestSvcbPacket = null; 119 120 // Used to check whether a DDR result is stale. 121 // Given the Threading section documented near the beginning of this file, `mTokenId` ensures 122 // that mLatestSvcbRecord is always fresh. 123 @NonNull 124 private int mTokenId; 125 126 // Used to cancel the in-progress SVCB lookup. 127 @NonNull 128 CancellationSignal mCancelSignal; 129 130 private final SharedLog mValidationLogs; 131 DdrTracker(@onNull Network cleartextDnsNetwork, @NonNull DnsResolver dnsResolver, @NonNull Executor executor, @NonNull Callback callback, SharedLog validationLog)132 DdrTracker(@NonNull Network cleartextDnsNetwork, @NonNull DnsResolver dnsResolver, 133 @NonNull Executor executor, @NonNull Callback callback, SharedLog validationLog) { 134 mCleartextDnsNetwork = cleartextDnsNetwork; 135 mDnsResolver = dnsResolver; 136 mExecutor = executor; 137 mCallback = callback; 138 final PrivateDnsConfig privateDnsDisabled = new PrivateDnsConfig(PRIVATE_DNS_MODE_OFF, 139 null /* hostname */, null /* ips */, true /* ddrEnabled */, null /* dohName */, 140 null /* dohIps */, null /* dohPath */, -1 /* dohPort */); 141 mDnsInfo = new DnsInfo(privateDnsDisabled, new ArrayList<>()); 142 mDotServers = new ArrayList<>(); 143 mCancelSignal = new CancellationSignal(); 144 mValidationLogs = validationLog.forSubComponent(TAG); 145 } 146 147 /** 148 * If the private DNS settings on the network has changed, this function updates 149 * the DnsInfo and returns true; otherwise, the DnsInfo remains the same and this function 150 * returns false. 151 */ notifyPrivateDnsSettingsChanged(@onNull PrivateDnsConfig cfg)152 boolean notifyPrivateDnsSettingsChanged(@NonNull PrivateDnsConfig cfg) { 153 if (mDnsInfo.cfg.areSettingsSameAs(cfg)) return false; 154 155 ++mTokenId; 156 mDnsInfo = new DnsInfo(cfg, getDnsServers()); 157 resetStrictModeHostnameResolutionResult(); 158 return true; 159 } 160 161 /** 162 * If the unencrypted DNS server list on the network has changed (even if only the order has 163 * changed), this function updates the DnsInfo and returns true; otherwise, the DnsInfo remains 164 * unchanged and this function returns false. 165 * 166 * The reason that this method returns true even if only the order has changed is that 167 * DnsResolver returns a DNS answer to app side as soon as it receives a DNS response from 168 * a DNS server. Therefore, the DNS response from the first DNS server that supports DDR 169 * determines the DDR result. 170 */ notifyLinkPropertiesChanged(@onNull LinkProperties lp)171 boolean notifyLinkPropertiesChanged(@NonNull LinkProperties lp) { 172 final List<InetAddress> servers = lp.getDnsServers(); 173 174 if (servers.equals(getDnsServers())) return false; 175 176 ++mTokenId; 177 mDnsInfo = new DnsInfo(mDnsInfo.cfg, servers); 178 return true; 179 } 180 setStrictModeHostnameResolutionResult(@onNull InetAddress[] ips)181 void setStrictModeHostnameResolutionResult(@NonNull InetAddress[] ips) { 182 resetStrictModeHostnameResolutionResult(); 183 mDotServers.addAll(Arrays.asList(ips)); 184 } 185 resetStrictModeHostnameResolutionResult()186 void resetStrictModeHostnameResolutionResult() { 187 mDotServers.clear(); 188 } 189 190 @VisibleForTesting getPrivateDnsMode()191 @PrivateDnsMode int getPrivateDnsMode() { 192 return mDnsInfo.cfg.mode; 193 } 194 195 // Returns a non-empty string (strict mode) or an empty string (off/opportunistic mode) . 196 @VisibleForTesting 197 @NonNull getStrictModeHostname()198 String getStrictModeHostname() { 199 return mDnsInfo.cfg.hostname; 200 } 201 202 @VisibleForTesting 203 @NonNull getDnsServers()204 List<InetAddress> getDnsServers() { 205 return mDnsInfo.dnsServers; 206 } 207 hasSvcbAnswer(@onNull String alpn)208 private boolean hasSvcbAnswer(@NonNull String alpn) { 209 return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.isSupported(alpn) : false; 210 } 211 212 @Nullable getTargetNameFromSvcbAnswer(@onNull String alpn)213 private String getTargetNameFromSvcbAnswer(@NonNull String alpn) { 214 return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getTargetName(alpn) : null; 215 } 216 217 // Returns a list of IP addresses for the target name from the latest SVCB packet. 218 // These may be either from the A/AAAA records in the additional section or from the 219 // ipv4hint/ipv6hint keys in the SVCB record. getServersFromSvcbAnswer(@onNull String alpn)220 private List<InetAddress> getServersFromSvcbAnswer(@NonNull String alpn) { 221 return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getAddresses(alpn) 222 : Collections.emptyList(); 223 } 224 getPortFromSvcbAnswer(@onNull String alpn)225 private int getPortFromSvcbAnswer(@NonNull String alpn) { 226 return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getPort(alpn) : -1; 227 } 228 229 @Nullable getDohPathFromSvcbAnswer(@onNull String alpn)230 private String getDohPathFromSvcbAnswer(@NonNull String alpn) { 231 return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getDohPath(alpn) : null; 232 } 233 234 @NonNull createHostnameForSvcbQuery()235 private String createHostnameForSvcbQuery() { 236 final String hostname = getStrictModeHostname(); 237 if (!TextUtils.isEmpty(hostname)) { 238 return "_dns." + hostname; 239 } 240 return DDR_HOSTNAME; 241 } 242 243 /** Performs a DNS SVCB Lookup asynchronously. */ startSvcbLookup()244 void startSvcbLookup() { 245 if (getPrivateDnsMode() == PRIVATE_DNS_MODE_OFF) { 246 // Ensure getResultForReporting returns reasonable results. 247 mLatestSvcbPacket = null; 248 // We do not need to increment the token. The token is used to ignore stale results. 249 // But there can only be lookups in flight if the mode was previously on. Because the 250 // mode is now off, that means the mode changed, and that incremented the token. 251 return; 252 } 253 // There are some cases where startSvcbLookup() is called twice in a row that 254 // are likely to lead to the same result, for example: 255 // 1. A network is connected when private DNS mode is strict mode. 256 // 2. Private DNS mode is switched to strict mode. 257 // To avoid duplicate lookups, cancel the in-progress SVCB lookup (if any). 258 // 259 // Note that cancelling is not currently very useful because the DNS resolver still 260 // continues to retry until the query completes or fails. It does prevent the query callback 261 // from being called, but that's not necessary because the token will not match. 262 // We still do attempt to cancel the query so future improvements to the DNS resolver could 263 // use that to do less work. 264 mCancelSignal.cancel(); 265 mCancelSignal = new CancellationSignal(); 266 267 // Increment the token ID to stale all in-flight lookups. 268 // This is for network revalidation in strict mode that a SVCB lookup can be performed 269 // and its result can be accepted even if there is no DNS configuration change. 270 final int token = ++mTokenId; 271 final String hostname = createHostnameForSvcbQuery(); 272 final DnsResolver.Callback<byte[]> callback = new DnsResolver.Callback<byte[]>() { 273 boolean isResultFresh() { 274 return token == mTokenId; 275 } 276 277 void updateSvcbAnswerAndInvokeUserCallback(@Nullable DnsSvcbPacket result) { 278 mLatestSvcbPacket = result; 279 mCallback.onSvcbLookupComplete(getResultForReporting()); 280 } 281 282 @Override 283 public void onAnswer(@NonNull byte[] answer, int rcode) { 284 if (!isResultFresh()) { 285 validationLog("Ignoring stale SVCB answer"); 286 return; 287 } 288 289 if (rcode != 0 || answer.length == 0) { 290 validationLog("Ignoring invalid SVCB answer: rcode=" + rcode 291 + " len=" + answer.length); 292 updateSvcbAnswerAndInvokeUserCallback(null); 293 return; 294 } 295 296 final DnsSvcbPacket pkt; 297 try { 298 pkt = DnsSvcbPacket.fromResponse(answer); 299 } catch (DnsPacket.ParseException e) { 300 validationLog("Ignoring malformed SVCB answer: " + e); 301 updateSvcbAnswerAndInvokeUserCallback(null); 302 return; 303 } 304 305 validationLog("Processing SVCB response: " + pkt); 306 updateSvcbAnswerAndInvokeUserCallback(pkt); 307 } 308 309 @Override 310 public void onError(@NonNull DnsResolver.DnsException e) { 311 validationLog("DNS error resolving SVCB record for " + hostname + ": " + e); 312 if (isResultFresh()) { 313 updateSvcbAnswerAndInvokeUserCallback(null); 314 } 315 } 316 }; 317 sendDnsSvcbQuery(hostname, mCancelSignal, callback); 318 } 319 320 /** 321 * Returns candidate IP addresses to use for DoH. 322 * 323 * These can come from A/AAAA records returned by strict mode hostname resolution, from A/AAAA 324 * records in the additional section of the SVCB response, or from the ipv4hint/ipv6hint keys in 325 * the H3 ALPN of the SVCB record itself. 326 * 327 * RFC 9460 §7.3 says that if A and AAAA records for TargetName are locally available, the 328 * client SHOULD ignore the hints. 329 * 330 * - In opportunistic mode, strict name hostname resolution does not happen, so always use the 331 * addresses in the SVCB response 332 * - In strict mode: 333 * - If the target name in the H3 ALPN matches the strict mode hostname, prefer the result of 334 * strict mode hostname resolution. 335 * - If not, prefer the addresses from the SVCB response, but fall back to A/AAAA records if 336 * there are none. This ensures that: 337 * - If the strict mode hostname has A/AAAA addresses, those are used even if there are no 338 * addresses in the SVCB record. 339 * 340 * Note that in strict mode, this class always uses the user-specified hostname and ignores the 341 * target hostname in the SVCB record (see getResultForReporting). In this case, preferring the 342 * addresses in the SVCB record at ensures that those addresses are used, even if the target 343 * hostname is not. 344 */ getTargetNameIpAddresses(@onNull String alpn)345 private List<InetAddress> getTargetNameIpAddresses(@NonNull String alpn) { 346 final List<InetAddress> serversFromSvcbAnswer = getServersFromSvcbAnswer(alpn); 347 final String hostname = getStrictModeHostname(); 348 if (TextUtils.isEmpty(hostname)) { 349 return serversFromSvcbAnswer; 350 } 351 // Strict mode can use either A/AAAA records coming from strict mode resolution or the 352 // addresses from the SVCB response (which could be A/AAAA records in the additional section 353 // or the hints in the SVCB record itself). 354 final String targetName = getTargetNameFromSvcbAnswer(alpn); 355 if (TextUtils.equals(targetName, hostname) && !mDotServers.isEmpty()) { 356 return mDotServers; 357 } 358 if (isEmpty(serversFromSvcbAnswer)) { 359 return mDotServers; 360 } 361 return serversFromSvcbAnswer; 362 } 363 364 /** 365 * To follow the design of private DNS opportunistic mode, which is similar to RFC 9462 §4.3, 366 * don't use a designated resolver if its IP address differs from all the unencrypted resolvers' 367 * IP addresses. 368 * 369 * TODO: simplify the code by merging this method with getTargetNameIpAddresses above. 370 */ getDohServers(@onNull String alpn)371 private InetAddress[] getDohServers(@NonNull String alpn) { 372 final List<InetAddress> candidates = getTargetNameIpAddresses(alpn); 373 if (isEmpty(candidates)) return null; 374 if (getPrivateDnsMode() == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) return toArray(candidates); 375 376 candidates.retainAll(getDnsServers()); 377 return toArray(candidates); 378 } 379 380 /** 381 * Returns the aggregated private DNS discovery result as a PrivateDnsConfig. 382 * getResultForReporting() is called in the following cases: 383 * 1. when the hostname lookup completes. 384 * 2. when the SVCB lookup completes. 385 * 386 * There is no guarantee which lookup will complete first. Therefore, depending on the private 387 * DNS mode and the SVCB answer, the return PrivateDnsConfig might be set with DoT, DoH, 388 * DoT+DoH, or even no servers. 389 */ 390 @NonNull getResultForReporting()391 PrivateDnsConfig getResultForReporting() { 392 final String strictModeHostname = getStrictModeHostname(); 393 final InetAddress[] dotIps = toArray(mDotServers); 394 final PrivateDnsConfig candidateResultWithDotOnly = 395 new PrivateDnsConfig(getPrivateDnsMode(), strictModeHostname, dotIps, 396 true /* ddrEnabled */, null /* dohName */, null /* dohIps */, 397 null /* dohPath */, -1 /* dohPort */); 398 399 if (!hasSvcbAnswer(ALPN_DOH3)) { 400 // TODO(b/240259333): Consider not invoking notifyPrivateDnsConfigResolved() if 401 // DoT server list is empty. 402 return candidateResultWithDotOnly; 403 } 404 405 // The SVCB answer should be fresh. 406 407 final String dohName = (getPrivateDnsMode() == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) 408 ? strictModeHostname : getTargetNameFromSvcbAnswer(ALPN_DOH3); 409 final InetAddress[] dohIps = getDohServers(ALPN_DOH3); 410 final String dohPath = getDohPathFromSvcbAnswer(ALPN_DOH3); 411 final int dohPort = getPortFromSvcbAnswer(ALPN_DOH3); 412 413 return new PrivateDnsConfig(getPrivateDnsMode(), strictModeHostname, dotIps, true, 414 dohName, dohIps, dohPath, dohPort); 415 } 416 validationLog(String s)417 private void validationLog(String s) { 418 log(s); 419 mValidationLogs.log(s); 420 } 421 log(String s)422 private void log(String s) { 423 if (DBG) Log.d(TAG + "/" + mCleartextDnsNetwork.toString(), s); 424 } 425 426 /** 427 * A non-blocking call doing DNS SVCB lookup. 428 */ sendDnsSvcbQuery(String host, @NonNull CancellationSignal cancelSignal, @NonNull DnsResolver.Callback<byte[]> callback)429 private void sendDnsSvcbQuery(String host, @NonNull CancellationSignal cancelSignal, 430 @NonNull DnsResolver.Callback<byte[]> callback) { 431 // Note: the even though this code does not pass FLAG_NO_CACHE_LOOKUP, the query is 432 // currently not cached, because the DNS resolver cache does not cache SVCB records. 433 // TODO: support caching SVCB records in the DNS resolver cache. 434 // This should just work but will need testing. 435 mDnsResolver.rawQuery(mCleartextDnsNetwork, host, CLASS_IN, TYPE_SVCB, 0 /* flags */, 436 mExecutor, cancelSignal, callback); 437 } 438 toArray(List<InetAddress> list)439 private static InetAddress[] toArray(List<InetAddress> list) { 440 if (list == null) { 441 return null; 442 } 443 return list.toArray(new InetAddress[0]); 444 } 445 446 /** 447 * A class to store current DNS configuration. Only the information relevant to DDR is stored. 448 * 1. Private DNS setting. 449 * 2. A list of Unencrypted DNS servers. 450 */ 451 private static class DnsInfo { 452 @NonNull 453 public final PrivateDnsConfig cfg; 454 @NonNull 455 public final List<InetAddress> dnsServers; 456 DnsInfo(@onNull PrivateDnsConfig cfg, @NonNull List<InetAddress> dnsServers)457 DnsInfo(@NonNull PrivateDnsConfig cfg, @NonNull List<InetAddress> dnsServers) { 458 this.cfg = cfg; 459 this.dnsServers = dnsServers; 460 } 461 } 462 } 463