• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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