1 /*
2  * Copyright (C) 2014 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.CaptivePortal.APP_RETURN_DISMISSED;
20 import static android.net.CaptivePortal.APP_RETURN_UNWANTED;
21 import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
22 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC;
23 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL;
24 import static android.net.metrics.ValidationProbeEvent.PROBE_FALLBACK;
25 
26 import android.annotation.Nullable;
27 import android.app.PendingIntent;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.net.CaptivePortal;
33 import android.net.ConnectivityManager;
34 import android.net.ICaptivePortal;
35 import android.net.Network;
36 import android.net.NetworkCapabilities;
37 import android.net.NetworkRequest;
38 import android.net.ProxyInfo;
39 import android.net.TrafficStats;
40 import android.net.Uri;
41 import android.net.captiveportal.CaptivePortalProbeResult;
42 import android.net.captiveportal.CaptivePortalProbeSpec;
43 import android.net.dns.ResolvUtil;
44 import android.net.metrics.IpConnectivityLog;
45 import android.net.metrics.NetworkEvent;
46 import android.net.metrics.ValidationProbeEvent;
47 import android.net.util.Stopwatch;
48 import android.net.wifi.WifiInfo;
49 import android.net.wifi.WifiManager;
50 import android.os.Handler;
51 import android.os.Message;
52 import android.os.SystemClock;
53 import android.os.UserHandle;
54 import android.provider.Settings;
55 import android.telephony.CellIdentityCdma;
56 import android.telephony.CellIdentityGsm;
57 import android.telephony.CellIdentityLte;
58 import android.telephony.CellIdentityWcdma;
59 import android.telephony.CellInfo;
60 import android.telephony.CellInfoCdma;
61 import android.telephony.CellInfoGsm;
62 import android.telephony.CellInfoLte;
63 import android.telephony.CellInfoWcdma;
64 import android.telephony.TelephonyManager;
65 import android.text.TextUtils;
66 import android.util.LocalLog;
67 import android.util.LocalLog.ReadOnlyLocalLog;
68 import android.util.Log;
69 
70 import com.android.internal.annotations.VisibleForTesting;
71 import com.android.internal.util.ArrayUtils;
72 import com.android.internal.util.Protocol;
73 import com.android.internal.util.State;
74 import com.android.internal.util.StateMachine;
75 import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
76 
77 import java.io.IOException;
78 import java.net.HttpURLConnection;
79 import java.net.InetAddress;
80 import java.net.MalformedURLException;
81 import java.net.URL;
82 import java.net.UnknownHostException;
83 import java.util.ArrayList;
84 import java.util.Arrays;
85 import java.util.Collections;
86 import java.util.LinkedHashMap;
87 import java.util.List;
88 import java.util.Random;
89 import java.util.UUID;
90 import java.util.concurrent.CountDownLatch;
91 import java.util.concurrent.TimeUnit;
92 
93 /**
94  * {@hide}
95  */
96 public class NetworkMonitor extends StateMachine {
97     private static final String TAG = NetworkMonitor.class.getSimpleName();
98     private static final boolean DBG  = true;
99     private static final boolean VDBG = false;
100 
101     // Default configuration values for captive portal detection probes.
102     // TODO: append a random length parameter to the default HTTPS url.
103     // TODO: randomize browser version ids in the default User-Agent String.
104     private static final String DEFAULT_HTTPS_URL     = "https://www.google.com/generate_204";
105     private static final String DEFAULT_HTTP_URL      =
106             "http://connectivitycheck.gstatic.com/generate_204";
107     private static final String DEFAULT_FALLBACK_URL  = "http://www.google.com/gen_204";
108     private static final String DEFAULT_OTHER_FALLBACK_URLS =
109             "http://play.googleapis.com/generate_204";
110     private static final String DEFAULT_USER_AGENT    = "Mozilla/5.0 (X11; Linux x86_64) "
111                                                       + "AppleWebKit/537.36 (KHTML, like Gecko) "
112                                                       + "Chrome/60.0.3112.32 Safari/537.36";
113 
114     private static final int SOCKET_TIMEOUT_MS = 10000;
115     private static final int PROBE_TIMEOUT_MS  = 3000;
116 
117     static enum EvaluationResult {
118         VALIDATED(true),
119         CAPTIVE_PORTAL(false);
120         final boolean isValidated;
EvaluationResult(boolean isValidated)121         EvaluationResult(boolean isValidated) {
122             this.isValidated = isValidated;
123         }
124     }
125 
126     static enum ValidationStage {
127         FIRST_VALIDATION(true),
128         REVALIDATION(false);
129         final boolean isFirstValidation;
ValidationStage(boolean isFirstValidation)130         ValidationStage(boolean isFirstValidation) {
131             this.isFirstValidation = isFirstValidation;
132         }
133     }
134 
135     // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
136     // The network should be used as a default internet connection.  It was found to be:
137     // 1. a functioning network providing internet access, or
138     // 2. a captive portal and the user decided to use it as is.
139     public static final int NETWORK_TEST_RESULT_VALID = 0;
140     // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
141     // The network should not be used as a default internet connection.  It was found to be:
142     // 1. a captive portal and the user is prompted to sign-in, or
143     // 2. a captive portal and the user did not want to use it, or
144     // 3. a broken network (e.g. DNS failed, connect failed, HTTP request failed).
145     public static final int NETWORK_TEST_RESULT_INVALID = 1;
146 
147     private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
148 
149     /**
150      * Inform NetworkMonitor that their network is connected.
151      * Initiates Network Validation.
152      */
153     public static final int CMD_NETWORK_CONNECTED = BASE + 1;
154 
155     /**
156      * Inform ConnectivityService that the network has been tested.
157      * obj = String representing URL that Internet probe was redirect to, if it was redirected.
158      * arg1 = One of the NETWORK_TESTED_RESULT_* constants.
159      * arg2 = NetID.
160      */
161     public static final int EVENT_NETWORK_TESTED = BASE + 2;
162 
163     /**
164      * Message to self indicating it's time to evaluate a network's connectivity.
165      * arg1 = Token to ignore old messages.
166      */
167     private static final int CMD_REEVALUATE = BASE + 6;
168 
169     /**
170      * Inform NetworkMonitor that the network has disconnected.
171      */
172     public static final int CMD_NETWORK_DISCONNECTED = BASE + 7;
173 
174     /**
175      * Force evaluation even if it has succeeded in the past.
176      * arg1 = UID responsible for requesting this reeval.  Will be billed for data.
177      */
178     private static final int CMD_FORCE_REEVALUATION = BASE + 8;
179 
180     /**
181      * Message to self indicating captive portal app finished.
182      * arg1 = one of: APP_RETURN_DISMISSED,
183      *                APP_RETURN_UNWANTED,
184      *                APP_RETURN_WANTED_AS_IS
185      * obj = mCaptivePortalLoggedInResponseToken as String
186      */
187     private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = BASE + 9;
188 
189     /**
190      * Request ConnectivityService display provisioning notification.
191      * arg1    = Whether to make the notification visible.
192      * arg2    = NetID.
193      * obj     = Intent to be launched when notification selected by user, null if !arg1.
194      */
195     public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 10;
196 
197     /**
198      * Message indicating sign-in app should be launched.
199      * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the
200      * user touches the sign in notification, or sent by
201      * ConnectivityService when the user touches the "sign into
202      * network" button in the wifi access point detail page.
203      */
204     public static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = BASE + 11;
205 
206     /**
207      * Retest network to see if captive portal is still in place.
208      * arg1 = UID responsible for requesting this reeval.  Will be billed for data.
209      *        0 indicates self-initiated, so nobody to blame.
210      */
211     private static final int CMD_CAPTIVE_PORTAL_RECHECK = BASE + 12;
212 
213     /**
214      * ConnectivityService notifies NetworkMonitor of settings changes to
215      * Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
216      * strict mode, then an event is sent back to ConnectivityService with the
217      * result of the resolution attempt.
218      *
219      * A separate message is used to trigger (re)evaluation of the Private DNS
220      * configuration, so that the message can be handled as needed in different
221      * states, including being ignored until after an ongoing captive portal
222      * validation phase is completed.
223      */
224     private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = BASE + 13;
225     public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = BASE + 14;
226     private static final int CMD_EVALUATE_PRIVATE_DNS = BASE + 15;
227 
228     // Start mReevaluateDelayMs at this value and double.
229     private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
230     private static final int MAX_REEVALUATE_DELAY_MS = 10*60*1000;
231     // Before network has been evaluated this many times, ignore repeated reevaluate requests.
232     private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
233     private int mReevaluateToken = 0;
234     private static final int NO_UID = 0;
235     private static final int INVALID_UID = -1;
236     private int mUidResponsibleForReeval = INVALID_UID;
237     // Stop blaming UID that requested re-evaluation after this many attempts.
238     private static final int BLAME_FOR_EVALUATION_ATTEMPTS = 5;
239     // Delay between reevaluations once a captive portal has been found.
240     private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10*60*1000;
241 
242     private static final int NUM_VALIDATION_LOG_LINES = 20;
243 
244     private String mPrivateDnsProviderHostname = "";
245 
isValidationRequired( NetworkCapabilities dfltNetCap, NetworkCapabilities nc)246     public static boolean isValidationRequired(
247             NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
248         // TODO: Consider requiring validation for DUN networks.
249         return dfltNetCap.satisfiedByNetworkCapabilities(nc);
250     }
251 
252     private final Context mContext;
253     private final Handler mConnectivityServiceHandler;
254     private final NetworkAgentInfo mNetworkAgentInfo;
255     private final Network mNetwork;
256     private final int mNetId;
257     private final TelephonyManager mTelephonyManager;
258     private final WifiManager mWifiManager;
259     private final NetworkRequest mDefaultRequest;
260     private final IpConnectivityLog mMetricsLog;
261     private final NetworkMonitorSettings mSettings;
262 
263     // Configuration values for captive portal detection probes.
264     private final String mCaptivePortalUserAgent;
265     private final URL mCaptivePortalHttpsUrl;
266     private final URL mCaptivePortalHttpUrl;
267     private final URL[] mCaptivePortalFallbackUrls;
268     @Nullable
269     private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs;
270 
271     @VisibleForTesting
272     protected boolean mIsCaptivePortalCheckEnabled;
273 
274     private boolean mUseHttps;
275     // The total number of captive portal detection attempts for this NetworkMonitor instance.
276     private int mValidations = 0;
277 
278     // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
279     private boolean mUserDoesNotWant = false;
280     // Avoids surfacing "Sign in to network" notification.
281     private boolean mDontDisplaySigninNotification = false;
282 
283     public boolean systemReady = false;
284 
285     private final State mDefaultState = new DefaultState();
286     private final State mValidatedState = new ValidatedState();
287     private final State mMaybeNotifyState = new MaybeNotifyState();
288     private final State mEvaluatingState = new EvaluatingState();
289     private final State mCaptivePortalState = new CaptivePortalState();
290     private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
291 
292     private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
293 
294     private final LocalLog validationLogs = new LocalLog(NUM_VALIDATION_LOG_LINES);
295 
296     private final Stopwatch mEvaluationTimer = new Stopwatch();
297 
298     // This variable is set before transitioning to the mCaptivePortalState.
299     private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED;
300 
301     private int mNextFallbackUrlIndex = 0;
302 
NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest)303     public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
304             NetworkRequest defaultRequest) {
305         this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog(),
306                 NetworkMonitorSettings.DEFAULT);
307     }
308 
309     @VisibleForTesting
NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest, IpConnectivityLog logger, NetworkMonitorSettings settings)310     protected NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
311             NetworkRequest defaultRequest, IpConnectivityLog logger,
312             NetworkMonitorSettings settings) {
313         // Add suffix indicating which NetworkMonitor we're talking about.
314         super(TAG + networkAgentInfo.name());
315 
316         // Logs with a tag of the form given just above, e.g.
317         //     <timestamp>   862  2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
318         setDbg(VDBG);
319 
320         mContext = context;
321         mMetricsLog = logger;
322         mConnectivityServiceHandler = handler;
323         mSettings = settings;
324         mNetworkAgentInfo = networkAgentInfo;
325         mNetwork = new OneAddressPerFamilyNetwork(networkAgentInfo.network());
326         mNetId = mNetwork.netId;
327         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
328         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
329         mDefaultRequest = defaultRequest;
330 
331         addState(mDefaultState);
332         addState(mMaybeNotifyState, mDefaultState);
333             addState(mEvaluatingState, mMaybeNotifyState);
334             addState(mCaptivePortalState, mMaybeNotifyState);
335         addState(mEvaluatingPrivateDnsState, mDefaultState);
336         addState(mValidatedState, mDefaultState);
337         setInitialState(mDefaultState);
338 
339         mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
340         mUseHttps = getUseHttpsValidation();
341         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
342         mCaptivePortalHttpsUrl = makeURL(getCaptivePortalServerHttpsUrl());
343         mCaptivePortalHttpUrl = makeURL(getCaptivePortalServerHttpUrl(settings, context));
344         mCaptivePortalFallbackUrls = makeCaptivePortalFallbackUrls();
345         mCaptivePortalFallbackSpecs = makeCaptivePortalFallbackProbeSpecs();
346 
347         start();
348     }
349 
forceReevaluation(int responsibleUid)350     public void forceReevaluation(int responsibleUid) {
351         sendMessage(CMD_FORCE_REEVALUATION, responsibleUid, 0);
352     }
353 
notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg)354     public void notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg) {
355         // Cancel any outstanding resolutions.
356         removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
357         // Send the update to the proper thread.
358         sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
359     }
360 
361     @Override
log(String s)362     protected void log(String s) {
363         if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
364     }
365 
validationLog(int probeType, Object url, String msg)366     private void validationLog(int probeType, Object url, String msg) {
367         String probeName = ValidationProbeEvent.getProbeName(probeType);
368         validationLog(String.format("%s %s %s", probeName, url, msg));
369     }
370 
validationLog(String s)371     private void validationLog(String s) {
372         if (DBG) log(s);
373         validationLogs.log(s);
374     }
375 
getValidationLogs()376     public ReadOnlyLocalLog getValidationLogs() {
377         return validationLogs.readOnlyLocalLog();
378     }
379 
validationStage()380     private ValidationStage validationStage() {
381         return 0 == mValidations ? ValidationStage.FIRST_VALIDATION : ValidationStage.REVALIDATION;
382     }
383 
isValidationRequired()384     private boolean isValidationRequired() {
385         return isValidationRequired(
386                 mDefaultRequest.networkCapabilities, mNetworkAgentInfo.networkCapabilities);
387     }
388 
389 
notifyNetworkTestResultInvalid(Object obj)390     private void notifyNetworkTestResultInvalid(Object obj) {
391         mConnectivityServiceHandler.sendMessage(obtainMessage(
392                 EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, obj));
393     }
394 
395     // DefaultState is the parent of all States.  It exists only to handle CMD_* messages but
396     // does not entail any real state (hence no enter() or exit() routines).
397     private class DefaultState extends State {
398         @Override
processMessage(Message message)399         public boolean processMessage(Message message) {
400             switch (message.what) {
401                 case CMD_NETWORK_CONNECTED:
402                     logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
403                     transitionTo(mEvaluatingState);
404                     return HANDLED;
405                 case CMD_NETWORK_DISCONNECTED:
406                     logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
407                     if (mLaunchCaptivePortalAppBroadcastReceiver != null) {
408                         mContext.unregisterReceiver(mLaunchCaptivePortalAppBroadcastReceiver);
409                         mLaunchCaptivePortalAppBroadcastReceiver = null;
410                     }
411                     quit();
412                     return HANDLED;
413                 case CMD_FORCE_REEVALUATION:
414                 case CMD_CAPTIVE_PORTAL_RECHECK:
415                     log("Forcing reevaluation for UID " + message.arg1);
416                     mUidResponsibleForReeval = message.arg1;
417                     transitionTo(mEvaluatingState);
418                     return HANDLED;
419                 case CMD_CAPTIVE_PORTAL_APP_FINISHED:
420                     log("CaptivePortal App responded with " + message.arg1);
421 
422                     // If the user has seen and acted on a captive portal notification, and the
423                     // captive portal app is now closed, disable HTTPS probes. This avoids the
424                     // following pathological situation:
425                     //
426                     // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out.
427                     // 2. User opens the app and logs into the captive portal.
428                     // 3. HTTP starts working, but HTTPS still doesn't work for some other reason -
429                     //    perhaps due to the network blocking HTTPS?
430                     //
431                     // In this case, we'll fail to validate the network even after the app is
432                     // dismissed. There is now no way to use this network, because the app is now
433                     // gone, so the user cannot select "Use this network as is".
434                     mUseHttps = false;
435 
436                     switch (message.arg1) {
437                         case APP_RETURN_DISMISSED:
438                             sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
439                             break;
440                         case APP_RETURN_WANTED_AS_IS:
441                             mDontDisplaySigninNotification = true;
442                             // TODO: Distinguish this from a network that actually validates.
443                             // Displaying the "x" on the system UI icon may still be a good idea.
444                             transitionTo(mEvaluatingPrivateDnsState);
445                             break;
446                         case APP_RETURN_UNWANTED:
447                             mDontDisplaySigninNotification = true;
448                             mUserDoesNotWant = true;
449                             notifyNetworkTestResultInvalid(null);
450                             // TODO: Should teardown network.
451                             mUidResponsibleForReeval = 0;
452                             transitionTo(mEvaluatingState);
453                             break;
454                     }
455                     return HANDLED;
456                 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
457                     final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
458                     if (!isValidationRequired() || cfg == null || !cfg.inStrictMode()) {
459                         // No DNS resolution required.
460                         //
461                         // We don't force any validation in opportunistic mode
462                         // here. Opportunistic mode nameservers are validated
463                         // separately within netd.
464                         //
465                         // Reset Private DNS settings state.
466                         mPrivateDnsProviderHostname = "";
467                         break;
468                     }
469 
470                     mPrivateDnsProviderHostname = cfg.hostname;
471 
472                     // DNS resolutions via Private DNS strict mode block for a
473                     // few seconds (~4.2) checking for any IP addresses to
474                     // arrive and validate. Initiating a (re)evaluation now
475                     // should not significantly alter the validation outcome.
476                     //
477                     // No matter what: enqueue a validation request; one of
478                     // three things can happen with this request:
479                     //     [1] ignored (EvaluatingState or CaptivePortalState)
480                     //     [2] transition to EvaluatingPrivateDnsState
481                     //         (DefaultState and ValidatedState)
482                     //     [3] handled (EvaluatingPrivateDnsState)
483                     //
484                     // The Private DNS configuration to be evaluated will:
485                     //     [1] be skipped (not in strict mode), or
486                     //     [2] validate (huzzah), or
487                     //     [3] encounter some problem (invalid hostname,
488                     //         no resolved IP addresses, IPs unreachable,
489                     //         port 853 unreachable, port 853 is not running a
490                     //         DNS-over-TLS server, et cetera).
491                     sendMessage(CMD_EVALUATE_PRIVATE_DNS);
492                     break;
493                 }
494                 default:
495                     break;
496             }
497             return HANDLED;
498         }
499     }
500 
501     // Being in the ValidatedState State indicates a Network is:
502     // - Successfully validated, or
503     // - Wanted "as is" by the user, or
504     // - Does not satisfy the default NetworkRequest and so validation has been skipped.
505     private class ValidatedState extends State {
506         @Override
enter()507         public void enter() {
508             maybeLogEvaluationResult(
509                     networkEventType(validationStage(), EvaluationResult.VALIDATED));
510             mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
511                     NETWORK_TEST_RESULT_VALID, mNetId, null));
512             mValidations++;
513         }
514 
515         @Override
processMessage(Message message)516         public boolean processMessage(Message message) {
517             switch (message.what) {
518                 case CMD_NETWORK_CONNECTED:
519                     transitionTo(mValidatedState);
520                     break;
521                 case CMD_EVALUATE_PRIVATE_DNS:
522                     transitionTo(mEvaluatingPrivateDnsState);
523                     break;
524                 default:
525                     return NOT_HANDLED;
526             }
527             return HANDLED;
528         }
529     }
530 
531     // Being in the MaybeNotifyState State indicates the user may have been notified that sign-in
532     // is required.  This State takes care to clear the notification upon exit from the State.
533     private class MaybeNotifyState extends State {
534         @Override
processMessage(Message message)535         public boolean processMessage(Message message) {
536             switch (message.what) {
537                 case CMD_LAUNCH_CAPTIVE_PORTAL_APP:
538                     final Intent intent = new Intent(
539                             ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
540                     // OneAddressPerFamilyNetwork is not parcelable across processes.
541                     intent.putExtra(ConnectivityManager.EXTRA_NETWORK, new Network(mNetwork));
542                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
543                             new CaptivePortal(new ICaptivePortal.Stub() {
544                                 @Override
545                                 public void appResponse(int response) {
546                                     if (response == APP_RETURN_WANTED_AS_IS) {
547                                         mContext.enforceCallingPermission(
548                                                 android.Manifest.permission.CONNECTIVITY_INTERNAL,
549                                                 "CaptivePortal");
550                                     }
551                                     sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response);
552                                 }
553                             }));
554                     final CaptivePortalProbeResult probeRes = mLastPortalProbeResult;
555                     intent.putExtra(EXTRA_CAPTIVE_PORTAL_URL, probeRes.detectUrl);
556                     if (probeRes.probeSpec != null) {
557                         final String encodedSpec = probeRes.probeSpec.getEncodedSpec();
558                         intent.putExtra(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC, encodedSpec);
559                     }
560                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT,
561                             mCaptivePortalUserAgent);
562                     intent.setFlags(
563                             Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
564                     mContext.startActivityAsUser(intent, UserHandle.CURRENT);
565                     return HANDLED;
566                 default:
567                     return NOT_HANDLED;
568             }
569         }
570 
571         @Override
exit()572         public void exit() {
573             Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, mNetId, null);
574             mConnectivityServiceHandler.sendMessage(message);
575         }
576     }
577 
578     // Being in the EvaluatingState State indicates the Network is being evaluated for internet
579     // connectivity, or that the user has indicated that this network is unwanted.
580     private class EvaluatingState extends State {
581         private int mReevaluateDelayMs;
582         private int mAttempts;
583 
584         @Override
enter()585         public void enter() {
586             // If we have already started to track time spent in EvaluatingState
587             // don't reset the timer due simply to, say, commands or events that
588             // cause us to exit and re-enter EvaluatingState.
589             if (!mEvaluationTimer.isStarted()) {
590                 mEvaluationTimer.start();
591             }
592             sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
593             if (mUidResponsibleForReeval != INVALID_UID) {
594                 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
595                 mUidResponsibleForReeval = INVALID_UID;
596             }
597             mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
598             mAttempts = 0;
599         }
600 
601         @Override
processMessage(Message message)602         public boolean processMessage(Message message) {
603             switch (message.what) {
604                 case CMD_REEVALUATE:
605                     if (message.arg1 != mReevaluateToken || mUserDoesNotWant)
606                         return HANDLED;
607                     // Don't bother validating networks that don't satisfy the default request.
608                     // This includes:
609                     //  - VPNs which can be considered explicitly desired by the user and the
610                     //    user's desire trumps whether the network validates.
611                     //  - Networks that don't provide Internet access.  It's unclear how to
612                     //    validate such networks.
613                     //  - Untrusted networks.  It's unsafe to prompt the user to sign-in to
614                     //    such networks and the user didn't express interest in connecting to
615                     //    such networks (an app did) so the user may be unhappily surprised when
616                     //    asked to sign-in to a network they didn't want to connect to in the
617                     //    first place.  Validation could be done to adjust the network scores
618                     //    however these networks are app-requested and may not be intended for
619                     //    general usage, in which case general validation may not be an accurate
620                     //    measure of the network's quality.  Only the app knows how to evaluate
621                     //    the network so don't bother validating here.  Furthermore sending HTTP
622                     //    packets over the network may be undesirable, for example an extremely
623                     //    expensive metered network, or unwanted leaking of the User Agent string.
624                     if (!isValidationRequired()) {
625                         validationLog("Network would not satisfy default request, not validating");
626                         transitionTo(mValidatedState);
627                         return HANDLED;
628                     }
629                     mAttempts++;
630                     // Note: This call to isCaptivePortal() could take up to a minute. Resolving the
631                     // server's IP addresses could hit the DNS timeout, and attempting connections
632                     // to each of the server's several IP addresses (currently one IPv4 and one
633                     // IPv6) could each take SOCKET_TIMEOUT_MS.  During this time this StateMachine
634                     // will be unresponsive. isCaptivePortal() could be executed on another Thread
635                     // if this is found to cause problems.
636                     CaptivePortalProbeResult probeResult = isCaptivePortal();
637                     if (probeResult.isSuccessful()) {
638                         // Transit EvaluatingPrivateDnsState to get to Validated
639                         // state (even if no Private DNS validation required).
640                         transitionTo(mEvaluatingPrivateDnsState);
641                     } else if (probeResult.isPortal()) {
642                         notifyNetworkTestResultInvalid(probeResult.redirectUrl);
643                         mLastPortalProbeResult = probeResult;
644                         transitionTo(mCaptivePortalState);
645                     } else {
646                         final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
647                         sendMessageDelayed(msg, mReevaluateDelayMs);
648                         logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
649                         notifyNetworkTestResultInvalid(probeResult.redirectUrl);
650                         if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
651                             // Don't continue to blame UID forever.
652                             TrafficStats.clearThreadStatsUid();
653                         }
654                         mReevaluateDelayMs *= 2;
655                         if (mReevaluateDelayMs > MAX_REEVALUATE_DELAY_MS) {
656                             mReevaluateDelayMs = MAX_REEVALUATE_DELAY_MS;
657                         }
658                     }
659                     return HANDLED;
660                 case CMD_FORCE_REEVALUATION:
661                     // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made,
662                     // ignore any re-evaluation requests. After, restart the
663                     // evaluation process via EvaluatingState#enter.
664                     return (mAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED;
665                 default:
666                     return NOT_HANDLED;
667             }
668         }
669 
670         @Override
exit()671         public void exit() {
672             TrafficStats.clearThreadStatsUid();
673         }
674     }
675 
676     // BroadcastReceiver that waits for a particular Intent and then posts a message.
677     private class CustomIntentReceiver extends BroadcastReceiver {
678         private final int mToken;
679         private final int mWhat;
680         private final String mAction;
CustomIntentReceiver(String action, int token, int what)681         CustomIntentReceiver(String action, int token, int what) {
682             mToken = token;
683             mWhat = what;
684             mAction = action + "_" + mNetId + "_" + token;
685             mContext.registerReceiver(this, new IntentFilter(mAction));
686         }
getPendingIntent()687         public PendingIntent getPendingIntent() {
688             final Intent intent = new Intent(mAction);
689             intent.setPackage(mContext.getPackageName());
690             return PendingIntent.getBroadcast(mContext, 0, intent, 0);
691         }
692         @Override
onReceive(Context context, Intent intent)693         public void onReceive(Context context, Intent intent) {
694             if (intent.getAction().equals(mAction)) sendMessage(obtainMessage(mWhat, mToken));
695         }
696     }
697 
698     // Being in the CaptivePortalState State indicates a captive portal was detected and the user
699     // has been shown a notification to sign-in.
700     private class CaptivePortalState extends State {
701         private static final String ACTION_LAUNCH_CAPTIVE_PORTAL_APP =
702                 "android.net.netmon.launchCaptivePortalApp";
703 
704         @Override
enter()705         public void enter() {
706             maybeLogEvaluationResult(
707                     networkEventType(validationStage(), EvaluationResult.CAPTIVE_PORTAL));
708             // Don't annoy user with sign-in notifications.
709             if (mDontDisplaySigninNotification) return;
710             // Create a CustomIntentReceiver that sends us a
711             // CMD_LAUNCH_CAPTIVE_PORTAL_APP message when the user
712             // touches the notification.
713             if (mLaunchCaptivePortalAppBroadcastReceiver == null) {
714                 // Wait for result.
715                 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver(
716                         ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(),
717                         CMD_LAUNCH_CAPTIVE_PORTAL_APP);
718             }
719             // Display the sign in notification.
720             Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, mNetId,
721                     mLaunchCaptivePortalAppBroadcastReceiver.getPendingIntent());
722             mConnectivityServiceHandler.sendMessage(message);
723             // Retest for captive portal occasionally.
724             sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
725                     CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
726             mValidations++;
727         }
728 
729         @Override
exit()730         public void exit() {
731             removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
732         }
733     }
734 
735     private class EvaluatingPrivateDnsState extends State {
736         private int mPrivateDnsReevalDelayMs;
737         private PrivateDnsConfig mPrivateDnsConfig;
738 
739         @Override
enter()740         public void enter() {
741             mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
742             mPrivateDnsConfig = null;
743             sendMessage(CMD_EVALUATE_PRIVATE_DNS);
744         }
745 
746         @Override
processMessage(Message msg)747         public boolean processMessage(Message msg) {
748             switch (msg.what) {
749                 case CMD_EVALUATE_PRIVATE_DNS:
750                     if (inStrictMode()) {
751                         if (!isStrictModeHostnameResolved()) {
752                             resolveStrictModeHostname();
753 
754                             if (isStrictModeHostnameResolved()) {
755                                 notifyPrivateDnsConfigResolved();
756                             } else {
757                                 handlePrivateDnsEvaluationFailure();
758                                 break;
759                             }
760                         }
761 
762                         // Look up a one-time hostname, to bypass caching.
763                         //
764                         // Note that this will race with ConnectivityService
765                         // code programming the DNS-over-TLS server IP addresses
766                         // into netd (if invoked, above). If netd doesn't know
767                         // the IP addresses yet, or if the connections to the IP
768                         // addresses haven't yet been validated, netd will block
769                         // for up to a few seconds before failing the lookup.
770                         if (!sendPrivateDnsProbe()) {
771                             handlePrivateDnsEvaluationFailure();
772                             break;
773                         }
774                     }
775 
776                     // All good!
777                     transitionTo(mValidatedState);
778                     break;
779                 default:
780                     return NOT_HANDLED;
781             }
782             return HANDLED;
783         }
784 
inStrictMode()785         private boolean inStrictMode() {
786             return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
787         }
788 
isStrictModeHostnameResolved()789         private boolean isStrictModeHostnameResolved() {
790             return (mPrivateDnsConfig != null) &&
791                    mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname) &&
792                    (mPrivateDnsConfig.ips.length > 0);
793         }
794 
resolveStrictModeHostname()795         private void resolveStrictModeHostname() {
796             try {
797                 // Do a blocking DNS resolution using the network-assigned nameservers.
798                 // Do not set AI_ADDRCONFIG in ai_flags so we get all address families in advance.
799                 final InetAddress[] ips = ResolvUtil.blockingResolveAllLocally(
800                         mNetwork, mPrivateDnsProviderHostname, 0 /* aiFlags */);
801                 mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
802             } catch (UnknownHostException uhe) {
803                 mPrivateDnsConfig = null;
804             }
805         }
806 
notifyPrivateDnsConfigResolved()807         private void notifyPrivateDnsConfigResolved() {
808             mConnectivityServiceHandler.sendMessage(obtainMessage(
809                     EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId, mPrivateDnsConfig));
810         }
811 
handlePrivateDnsEvaluationFailure()812         private void handlePrivateDnsEvaluationFailure() {
813             notifyNetworkTestResultInvalid(null);
814 
815             // Queue up a re-evaluation with backoff.
816             //
817             // TODO: Consider abandoning this state after a few attempts and
818             // transitioning back to EvaluatingState, to perhaps give ourselves
819             // the opportunity to (re)detect a captive portal or something.
820             sendMessageDelayed(CMD_EVALUATE_PRIVATE_DNS, mPrivateDnsReevalDelayMs);
821             mPrivateDnsReevalDelayMs *= 2;
822             if (mPrivateDnsReevalDelayMs > MAX_REEVALUATE_DELAY_MS) {
823                 mPrivateDnsReevalDelayMs = MAX_REEVALUATE_DELAY_MS;
824             }
825         }
826 
sendPrivateDnsProbe()827         private boolean sendPrivateDnsProbe() {
828             // q.v. system/netd/server/dns/DnsTlsTransport.cpp
829             final String ONE_TIME_HOSTNAME_SUFFIX = "-dnsotls-ds.metric.gstatic.com";
830             final String host = UUID.randomUUID().toString().substring(0, 8) +
831                     ONE_TIME_HOSTNAME_SUFFIX;
832             try {
833                 final InetAddress[] ips = mNetworkAgentInfo.network().getAllByName(host);
834                 return (ips != null && ips.length > 0);
835             } catch (UnknownHostException uhe) {}
836             return false;
837         }
838     }
839 
840     // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
841     // most one per address family. This ensures we only wait up to 20 seconds for TCP connections
842     // to complete, regardless of how many IP addresses a host has.
843     private static class OneAddressPerFamilyNetwork extends Network {
OneAddressPerFamilyNetwork(Network network)844         public OneAddressPerFamilyNetwork(Network network) {
845             super(network);
846         }
847 
848         @Override
getAllByName(String host)849         public InetAddress[] getAllByName(String host) throws UnknownHostException {
850             // Always bypass Private DNS.
851             final List<InetAddress> addrs = Arrays.asList(
852                     ResolvUtil.blockingResolveAllLocally(this, host));
853 
854             // Ensure the address family of the first address is tried first.
855             LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
856             addressByFamily.put(addrs.get(0).getClass(), addrs.get(0));
857             Collections.shuffle(addrs);
858 
859             for (InetAddress addr : addrs) {
860                 addressByFamily.put(addr.getClass(), addr);
861             }
862 
863             return addressByFamily.values().toArray(new InetAddress[addressByFamily.size()]);
864         }
865     }
866 
getIsCaptivePortalCheckEnabled()867     public boolean getIsCaptivePortalCheckEnabled() {
868         String symbol = Settings.Global.CAPTIVE_PORTAL_MODE;
869         int defaultValue = Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT;
870         int mode = mSettings.getSetting(mContext, symbol, defaultValue);
871         return mode != Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE;
872     }
873 
getUseHttpsValidation()874     public boolean getUseHttpsValidation() {
875         return mSettings.getSetting(mContext, Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
876     }
877 
getWifiScansAlwaysAvailableDisabled()878     public boolean getWifiScansAlwaysAvailableDisabled() {
879         return mSettings.getSetting(mContext, Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0;
880     }
881 
getCaptivePortalServerHttpsUrl()882     private String getCaptivePortalServerHttpsUrl() {
883         return mSettings.getSetting(mContext,
884                 Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
885     }
886 
887     // Static for direct access by ConnectivityService
getCaptivePortalServerHttpUrl(Context context)888     public static String getCaptivePortalServerHttpUrl(Context context) {
889         return getCaptivePortalServerHttpUrl(NetworkMonitorSettings.DEFAULT, context);
890     }
891 
getCaptivePortalServerHttpUrl( NetworkMonitorSettings settings, Context context)892     public static String getCaptivePortalServerHttpUrl(
893             NetworkMonitorSettings settings, Context context) {
894         return settings.getSetting(
895                 context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL);
896     }
897 
makeCaptivePortalFallbackUrls()898     private URL[] makeCaptivePortalFallbackUrls() {
899         try {
900             String separator = ",";
901             String firstUrl = mSettings.getSetting(mContext,
902                     Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL);
903             String joinedUrls = firstUrl + separator + mSettings.getSetting(mContext,
904                     Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS,
905                     DEFAULT_OTHER_FALLBACK_URLS);
906             List<URL> urls = new ArrayList<>();
907             for (String s : joinedUrls.split(separator)) {
908                 URL u = makeURL(s);
909                 if (u == null) {
910                     continue;
911                 }
912                 urls.add(u);
913             }
914             if (urls.isEmpty()) {
915                 Log.e(TAG, String.format("could not create any url from %s", joinedUrls));
916             }
917             return urls.toArray(new URL[urls.size()]);
918         } catch (Exception e) {
919             // Don't let a misconfiguration bootloop the system.
920             Log.e(TAG, "Error parsing configured fallback URLs", e);
921             return new URL[0];
922         }
923     }
924 
makeCaptivePortalFallbackProbeSpecs()925     private CaptivePortalProbeSpec[] makeCaptivePortalFallbackProbeSpecs() {
926         try {
927             final String settingsValue = mSettings.getSetting(
928                     mContext, Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS, null);
929             // Probe specs only used if configured in settings
930             if (TextUtils.isEmpty(settingsValue)) {
931                 return null;
932             }
933 
934             return CaptivePortalProbeSpec.parseCaptivePortalProbeSpecs(settingsValue);
935         } catch (Exception e) {
936             // Don't let a misconfiguration bootloop the system.
937             Log.e(TAG, "Error parsing configured fallback probe specs", e);
938             return null;
939         }
940     }
941 
getCaptivePortalUserAgent()942     private String getCaptivePortalUserAgent() {
943         return mSettings.getSetting(mContext,
944                 Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
945     }
946 
nextFallbackUrl()947     private URL nextFallbackUrl() {
948         if (mCaptivePortalFallbackUrls.length == 0) {
949             return null;
950         }
951         int idx = Math.abs(mNextFallbackUrlIndex) % mCaptivePortalFallbackUrls.length;
952         mNextFallbackUrlIndex += new Random().nextInt(); // randomely change url without memory.
953         return mCaptivePortalFallbackUrls[idx];
954     }
955 
nextFallbackSpec()956     private CaptivePortalProbeSpec nextFallbackSpec() {
957         if (ArrayUtils.isEmpty(mCaptivePortalFallbackSpecs)) {
958             return null;
959         }
960         // Randomly change spec without memory. Also randomize the first attempt.
961         final int idx = Math.abs(new Random().nextInt()) % mCaptivePortalFallbackSpecs.length;
962         return mCaptivePortalFallbackSpecs[idx];
963     }
964 
965     @VisibleForTesting
isCaptivePortal()966     protected CaptivePortalProbeResult isCaptivePortal() {
967         if (!mIsCaptivePortalCheckEnabled) {
968             validationLog("Validation disabled.");
969             return CaptivePortalProbeResult.SUCCESS;
970         }
971 
972         URL pacUrl = null;
973         URL httpsUrl = mCaptivePortalHttpsUrl;
974         URL httpUrl = mCaptivePortalHttpUrl;
975 
976         // On networks with a PAC instead of fetching a URL that should result in a 204
977         // response, we instead simply fetch the PAC script.  This is done for a few reasons:
978         // 1. At present our PAC code does not yet handle multiple PACs on multiple networks
979         //    until something like https://android-review.googlesource.com/#/c/115180/ lands.
980         //    Network.openConnection() will ignore network-specific PACs and instead fetch
981         //    using NO_PROXY.  If a PAC is in place, the only fetch we know will succeed with
982         //    NO_PROXY is the fetch of the PAC itself.
983         // 2. To proxy the generate_204 fetch through a PAC would require a number of things
984         //    happen before the fetch can commence, namely:
985         //        a) the PAC script be fetched
986         //        b) a PAC script resolver service be fired up and resolve the captive portal
987         //           server.
988         //    Network validation could be delayed until these prerequisities are satisifed or
989         //    could simply be left to race them.  Neither is an optimal solution.
990         // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
991         //    fact block fetching of the generate_204 URL which would lead to false negative
992         //    results for network validation.
993         final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
994         if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
995             pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
996             if (pacUrl == null) {
997                 return CaptivePortalProbeResult.FAILED;
998             }
999         }
1000 
1001         if ((pacUrl == null) && (httpUrl == null || httpsUrl == null)) {
1002             return CaptivePortalProbeResult.FAILED;
1003         }
1004 
1005         long startTime = SystemClock.elapsedRealtime();
1006 
1007         final CaptivePortalProbeResult result;
1008         if (pacUrl != null) {
1009             result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
1010         } else if (mUseHttps) {
1011             result = sendParallelHttpProbes(proxyInfo, httpsUrl, httpUrl);
1012         } else {
1013             result = sendDnsAndHttpProbes(proxyInfo, httpUrl, ValidationProbeEvent.PROBE_HTTP);
1014         }
1015 
1016         long endTime = SystemClock.elapsedRealtime();
1017 
1018         sendNetworkConditionsBroadcast(true /* response received */,
1019                 result.isPortal() /* isCaptivePortal */,
1020                 startTime, endTime);
1021 
1022         return result;
1023     }
1024 
1025     /**
1026      * Do a DNS resolution and URL fetch on a known web server to see if we get the data we expect.
1027      * @return a CaptivePortalProbeResult inferred from the HTTP response.
1028      */
sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType)1029     private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
1030         // Pre-resolve the captive portal server host so we can log it.
1031         // Only do this if HttpURLConnection is about to, to avoid any potentially
1032         // unnecessary resolution.
1033         final String host = (proxy != null) ? proxy.getHost() : url.getHost();
1034         sendDnsProbe(host);
1035         return sendHttpProbe(url, probeType, null);
1036     }
1037 
1038     /** Do a DNS resolution of the given server. */
sendDnsProbe(String host)1039     private void sendDnsProbe(String host) {
1040         if (TextUtils.isEmpty(host)) {
1041             return;
1042         }
1043 
1044         final String name = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
1045         final Stopwatch watch = new Stopwatch().start();
1046         int result;
1047         String connectInfo;
1048         try {
1049             InetAddress[] addresses = mNetwork.getAllByName(host);
1050             StringBuffer buffer = new StringBuffer();
1051             for (InetAddress address : addresses) {
1052                 buffer.append(',').append(address.getHostAddress());
1053             }
1054             result = ValidationProbeEvent.DNS_SUCCESS;
1055             connectInfo = "OK " + buffer.substring(1);
1056         } catch (UnknownHostException e) {
1057             result = ValidationProbeEvent.DNS_FAILURE;
1058             connectInfo = "FAIL";
1059         }
1060         final long latency = watch.stop();
1061         validationLog(ValidationProbeEvent.PROBE_DNS, host,
1062                 String.format("%dms %s", latency, connectInfo));
1063         logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result);
1064     }
1065 
1066     /**
1067      * Do a URL fetch on a known web server to see if we get the data we expect.
1068      * @return a CaptivePortalProbeResult inferred from the HTTP response.
1069      */
1070     @VisibleForTesting
sendHttpProbe(URL url, int probeType, @Nullable CaptivePortalProbeSpec probeSpec)1071     protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
1072             @Nullable CaptivePortalProbeSpec probeSpec) {
1073         HttpURLConnection urlConnection = null;
1074         int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
1075         String redirectUrl = null;
1076         final Stopwatch probeTimer = new Stopwatch().start();
1077         final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_PROBE);
1078         try {
1079             urlConnection = (HttpURLConnection) mNetwork.openConnection(url);
1080             urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
1081             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
1082             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
1083             urlConnection.setUseCaches(false);
1084             if (mCaptivePortalUserAgent != null) {
1085                 urlConnection.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
1086             }
1087             // cannot read request header after connection
1088             String requestHeader = urlConnection.getRequestProperties().toString();
1089 
1090             // Time how long it takes to get a response to our request
1091             long requestTimestamp = SystemClock.elapsedRealtime();
1092 
1093             httpResponseCode = urlConnection.getResponseCode();
1094             redirectUrl = urlConnection.getHeaderField("location");
1095 
1096             // Time how long it takes to get a response to our request
1097             long responseTimestamp = SystemClock.elapsedRealtime();
1098 
1099             validationLog(probeType, url, "time=" + (responseTimestamp - requestTimestamp) + "ms" +
1100                     " ret=" + httpResponseCode +
1101                     " request=" + requestHeader +
1102                     " headers=" + urlConnection.getHeaderFields());
1103             // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
1104             // portal.  The only example of this seen so far was a captive portal.  For
1105             // the time being go with prior behavior of assuming it's not a captive
1106             // portal.  If it is considered a captive portal, a different sign-in URL
1107             // is needed (i.e. can't browse a 204).  This could be the result of an HTTP
1108             // proxy server.
1109             if (httpResponseCode == 200) {
1110                 if (probeType == ValidationProbeEvent.PROBE_PAC) {
1111                     validationLog(
1112                             probeType, url, "PAC fetch 200 response interpreted as 204 response.");
1113                     httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1114                 } else if (urlConnection.getContentLengthLong() == 0) {
1115                     // Consider 200 response with "Content-length=0" to not be a captive portal.
1116                     // There's no point in considering this a captive portal as the user cannot
1117                     // sign-in to an empty page. Probably the result of a broken transparent proxy.
1118                     // See http://b/9972012.
1119                     validationLog(probeType, url,
1120                         "200 response with Content-length=0 interpreted as 204 response.");
1121                     httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1122                 } else if (urlConnection.getContentLengthLong() == -1) {
1123                     // When no Content-length (default value == -1), attempt to read a byte from the
1124                     // response. Do not use available() as it is unreliable. See http://b/33498325.
1125                     if (urlConnection.getInputStream().read() == -1) {
1126                         validationLog(
1127                                 probeType, url, "Empty 200 response interpreted as 204 response.");
1128                         httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1129                     }
1130                 }
1131             }
1132         } catch (IOException e) {
1133             validationLog(probeType, url, "Probe failed with exception " + e);
1134             if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
1135                 // TODO: Ping gateway and DNS server and log results.
1136             }
1137         } finally {
1138             if (urlConnection != null) {
1139                 urlConnection.disconnect();
1140             }
1141             TrafficStats.setThreadStatsTag(oldTag);
1142         }
1143         logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
1144 
1145         if (probeSpec == null) {
1146             return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString());
1147         } else {
1148             return probeSpec.getResult(httpResponseCode, redirectUrl);
1149         }
1150     }
1151 
sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl)1152     private CaptivePortalProbeResult sendParallelHttpProbes(
1153             ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
1154         // Number of probes to wait for. If a probe completes with a conclusive answer
1155         // it shortcuts the latch immediately by forcing the count to 0.
1156         final CountDownLatch latch = new CountDownLatch(2);
1157 
1158         final class ProbeThread extends Thread {
1159             private final boolean mIsHttps;
1160             private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;
1161 
1162             public ProbeThread(boolean isHttps) {
1163                 mIsHttps = isHttps;
1164             }
1165 
1166             public CaptivePortalProbeResult result() {
1167                 return mResult;
1168             }
1169 
1170             @Override
1171             public void run() {
1172                 if (mIsHttps) {
1173                     mResult =
1174                             sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
1175                 } else {
1176                     mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP);
1177                 }
1178                 if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
1179                     // Stop waiting immediately if https succeeds or if http finds a portal.
1180                     while (latch.getCount() > 0) {
1181                         latch.countDown();
1182                     }
1183                 }
1184                 // Signal this probe has completed.
1185                 latch.countDown();
1186             }
1187         }
1188 
1189         final ProbeThread httpsProbe = new ProbeThread(true);
1190         final ProbeThread httpProbe = new ProbeThread(false);
1191 
1192         try {
1193             httpsProbe.start();
1194             httpProbe.start();
1195             latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
1196         } catch (InterruptedException e) {
1197             validationLog("Error: probes wait interrupted!");
1198             return CaptivePortalProbeResult.FAILED;
1199         }
1200 
1201         final CaptivePortalProbeResult httpsResult = httpsProbe.result();
1202         final CaptivePortalProbeResult httpResult = httpProbe.result();
1203 
1204         // Look for a conclusive probe result first.
1205         if (httpResult.isPortal()) {
1206             return httpResult;
1207         }
1208         // httpsResult.isPortal() is not expected, but check it nonetheless.
1209         if (httpsResult.isPortal() || httpsResult.isSuccessful()) {
1210             return httpsResult;
1211         }
1212         // If a fallback method exists, use it to retry portal detection.
1213         // If we have new-style probe specs, use those. Otherwise, use the fallback URLs.
1214         final CaptivePortalProbeSpec probeSpec = nextFallbackSpec();
1215         final URL fallbackUrl = (probeSpec != null) ? probeSpec.getUrl() : nextFallbackUrl();
1216         if (fallbackUrl != null) {
1217             CaptivePortalProbeResult result = sendHttpProbe(fallbackUrl, PROBE_FALLBACK, probeSpec);
1218             if (result.isPortal()) {
1219                 return result;
1220             }
1221         }
1222         // Otherwise wait until http and https probes completes and use their results.
1223         try {
1224             httpProbe.join();
1225             if (httpProbe.result().isPortal()) {
1226                 return httpProbe.result();
1227             }
1228             httpsProbe.join();
1229             return httpsProbe.result();
1230         } catch (InterruptedException e) {
1231             validationLog("Error: http or https probe wait interrupted!");
1232             return CaptivePortalProbeResult.FAILED;
1233         }
1234     }
1235 
makeURL(String url)1236     private URL makeURL(String url) {
1237         if (url != null) {
1238             try {
1239                 return new URL(url);
1240             } catch (MalformedURLException e) {
1241                 validationLog("Bad URL: " + url);
1242             }
1243         }
1244         return null;
1245     }
1246 
1247     /**
1248      * @param responseReceived - whether or not we received a valid HTTP response to our request.
1249      * If false, isCaptivePortal and responseTimestampMs are ignored
1250      * TODO: This should be moved to the transports.  The latency could be passed to the transports
1251      * along with the captive portal result.  Currently the TYPE_MOBILE broadcasts appear unused so
1252      * perhaps this could just be added to the WiFi transport only.
1253      */
sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, long requestTimestampMs, long responseTimestampMs)1254     private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
1255             long requestTimestampMs, long responseTimestampMs) {
1256         if (getWifiScansAlwaysAvailableDisabled()) {
1257             return;
1258         }
1259 
1260         if (!systemReady) {
1261             return;
1262         }
1263 
1264         Intent latencyBroadcast =
1265                 new Intent(ConnectivityConstants.ACTION_NETWORK_CONDITIONS_MEASURED);
1266         switch (mNetworkAgentInfo.networkInfo.getType()) {
1267             case ConnectivityManager.TYPE_WIFI:
1268                 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
1269                 if (currentWifiInfo != null) {
1270                     // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
1271                     // surrounded by double quotation marks (thus violating the Javadoc), but this
1272                     // was changed to match the Javadoc in API 17. Since clients may have started
1273                     // sanitizing the output of this method since API 17 was released, we should
1274                     // not change it here as it would become impossible to tell whether the SSID is
1275                     // simply being surrounded by quotes due to the API, or whether those quotes
1276                     // are actually part of the SSID.
1277                     latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_SSID,
1278                             currentWifiInfo.getSSID());
1279                     latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_BSSID,
1280                             currentWifiInfo.getBSSID());
1281                 } else {
1282                     if (VDBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
1283                     return;
1284                 }
1285                 break;
1286             case ConnectivityManager.TYPE_MOBILE:
1287                 latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_NETWORK_TYPE,
1288                         mTelephonyManager.getNetworkType());
1289                 List<CellInfo> info = mTelephonyManager.getAllCellInfo();
1290                 if (info == null) return;
1291                 int numRegisteredCellInfo = 0;
1292                 for (CellInfo cellInfo : info) {
1293                     if (cellInfo.isRegistered()) {
1294                         numRegisteredCellInfo++;
1295                         if (numRegisteredCellInfo > 1) {
1296                             if (VDBG) logw("more than one registered CellInfo." +
1297                                     " Can't tell which is active.  Bailing.");
1298                             return;
1299                         }
1300                         if (cellInfo instanceof CellInfoCdma) {
1301                             CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
1302                             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
1303                         } else if (cellInfo instanceof CellInfoGsm) {
1304                             CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
1305                             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
1306                         } else if (cellInfo instanceof CellInfoLte) {
1307                             CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
1308                             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
1309                         } else if (cellInfo instanceof CellInfoWcdma) {
1310                             CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
1311                             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
1312                         } else {
1313                             if (VDBG) logw("Registered cellinfo is unrecognized");
1314                             return;
1315                         }
1316                     }
1317                 }
1318                 break;
1319             default:
1320                 return;
1321         }
1322         latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CONNECTIVITY_TYPE,
1323                 mNetworkAgentInfo.networkInfo.getType());
1324         latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_RECEIVED,
1325                 responseReceived);
1326         latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_REQUEST_TIMESTAMP_MS,
1327                 requestTimestampMs);
1328 
1329         if (responseReceived) {
1330             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_IS_CAPTIVE_PORTAL,
1331                     isCaptivePortal);
1332             latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_TIMESTAMP_MS,
1333                     responseTimestampMs);
1334         }
1335         mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT,
1336                 ConnectivityConstants.PERMISSION_ACCESS_NETWORK_CONDITIONS);
1337     }
1338 
logNetworkEvent(int evtype)1339     private void logNetworkEvent(int evtype) {
1340         int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
1341         mMetricsLog.log(mNetId, transports, new NetworkEvent(evtype));
1342     }
1343 
networkEventType(ValidationStage s, EvaluationResult r)1344     private int networkEventType(ValidationStage s, EvaluationResult r) {
1345         if (s.isFirstValidation) {
1346             if (r.isValidated) {
1347                 return NetworkEvent.NETWORK_FIRST_VALIDATION_SUCCESS;
1348             } else {
1349                 return NetworkEvent.NETWORK_FIRST_VALIDATION_PORTAL_FOUND;
1350             }
1351         } else {
1352             if (r.isValidated) {
1353                 return NetworkEvent.NETWORK_REVALIDATION_SUCCESS;
1354             } else {
1355                 return NetworkEvent.NETWORK_REVALIDATION_PORTAL_FOUND;
1356             }
1357         }
1358     }
1359 
maybeLogEvaluationResult(int evtype)1360     private void maybeLogEvaluationResult(int evtype) {
1361         if (mEvaluationTimer.isRunning()) {
1362             int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
1363             mMetricsLog.log(mNetId, transports, new NetworkEvent(evtype, mEvaluationTimer.stop()));
1364             mEvaluationTimer.reset();
1365         }
1366     }
1367 
logValidationProbe(long durationMs, int probeType, int probeResult)1368     private void logValidationProbe(long durationMs, int probeType, int probeResult) {
1369         int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
1370         boolean isFirstValidation = validationStage().isFirstValidation;
1371         ValidationProbeEvent ev = new ValidationProbeEvent();
1372         ev.probeType = ValidationProbeEvent.makeProbeType(probeType, isFirstValidation);
1373         ev.returnCode = probeResult;
1374         ev.durationMs = durationMs;
1375         mMetricsLog.log(mNetId, transports, ev);
1376     }
1377 
1378     @VisibleForTesting
1379     public interface NetworkMonitorSettings {
getSetting(Context context, String symbol, int defaultValue)1380         int getSetting(Context context, String symbol, int defaultValue);
getSetting(Context context, String symbol, String defaultValue)1381         String getSetting(Context context, String symbol, String defaultValue);
1382 
1383         static NetworkMonitorSettings DEFAULT = new DefaultNetworkMonitorSettings();
1384     }
1385 
1386     @VisibleForTesting
1387     public static class DefaultNetworkMonitorSettings implements NetworkMonitorSettings {
getSetting(Context context, String symbol, int defaultValue)1388         public int getSetting(Context context, String symbol, int defaultValue) {
1389             return Settings.Global.getInt(context.getContentResolver(), symbol, defaultValue);
1390         }
1391 
getSetting(Context context, String symbol, String defaultValue)1392         public String getSetting(Context context, String symbol, String defaultValue) {
1393             final String value = Settings.Global.getString(context.getContentResolver(), symbol);
1394             return value != null ? value : defaultValue;
1395         }
1396     }
1397 }
1398