blob: 1c803ddd1d679bde11c2bae5b4cd6dbb264f48ee [file] [log] [blame]
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001/*
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
17package com.android.server.connectivity;
18
19import static android.net.CaptivePortal.APP_RETURN_DISMISSED;
20import static android.net.CaptivePortal.APP_RETURN_UNWANTED;
21import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
22import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC;
23import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL;
24import static android.net.ConnectivityManager.TYPE_MOBILE;
25import static android.net.ConnectivityManager.TYPE_WIFI;
26import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +090027import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090028import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
29import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
30import static android.net.metrics.ValidationProbeEvent.DNS_FAILURE;
31import static android.net.metrics.ValidationProbeEvent.DNS_SUCCESS;
32import static android.net.metrics.ValidationProbeEvent.PROBE_FALLBACK;
33import static android.net.metrics.ValidationProbeEvent.PROBE_PRIVDNS;
Remi NGUYEN VANabeaaf72019-01-20 13:48:19 +090034import static android.net.util.NetworkStackUtils.isEmpty;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090035
Chiachang Wang8b5f84a2019-02-22 11:13:07 +080036import android.annotation.NonNull;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090037import android.annotation.Nullable;
38import android.app.PendingIntent;
39import android.content.BroadcastReceiver;
40import android.content.Context;
41import android.content.Intent;
42import android.content.IntentFilter;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090043import android.net.ConnectivityManager;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090044import android.net.INetworkMonitor;
45import android.net.INetworkMonitorCallbacks;
46import android.net.LinkProperties;
47import android.net.Network;
48import android.net.NetworkCapabilities;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090049import android.net.ProxyInfo;
50import android.net.TrafficStats;
51import android.net.Uri;
52import android.net.captiveportal.CaptivePortalProbeResult;
53import android.net.captiveportal.CaptivePortalProbeSpec;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +080054import android.net.metrics.DataStallDetectionStats;
55import android.net.metrics.DataStallStatsUtils;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090056import android.net.metrics.IpConnectivityLog;
57import android.net.metrics.NetworkEvent;
58import android.net.metrics.ValidationProbeEvent;
59import android.net.shared.NetworkMonitorUtils;
60import android.net.shared.PrivateDnsConfig;
61import android.net.util.SharedLog;
62import android.net.util.Stopwatch;
63import android.net.wifi.WifiInfo;
64import android.net.wifi.WifiManager;
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +090065import android.os.Bundle;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090066import android.os.Message;
67import android.os.RemoteException;
68import android.os.SystemClock;
69import android.os.UserHandle;
70import android.provider.Settings;
Chiachang Wangb04f81c2019-02-14 09:30:58 +080071import android.telephony.AccessNetworkConstants;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +080072import android.telephony.CellSignalStrength;
Chiachang Wangb04f81c2019-02-14 09:30:58 +080073import android.telephony.NetworkRegistrationState;
74import android.telephony.ServiceState;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +080075import android.telephony.SignalStrength;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090076import android.telephony.TelephonyManager;
77import android.text.TextUtils;
78import android.util.Log;
79
80import com.android.internal.annotations.VisibleForTesting;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090081import com.android.internal.util.RingBufferIndices;
82import com.android.internal.util.State;
83import com.android.internal.util.StateMachine;
84
85import java.io.IOException;
86import java.net.HttpURLConnection;
87import java.net.InetAddress;
88import java.net.MalformedURLException;
89import java.net.URL;
90import java.net.UnknownHostException;
91import java.util.ArrayList;
92import java.util.Arrays;
Remi NGUYEN VAN777022e2019-01-28 13:28:35 +090093import java.util.Collection;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090094import java.util.Collections;
95import java.util.LinkedHashMap;
96import java.util.List;
97import java.util.Random;
98import java.util.UUID;
99import java.util.concurrent.CountDownLatch;
100import java.util.concurrent.TimeUnit;
101
102/**
103 * {@hide}
104 */
105public class NetworkMonitor extends StateMachine {
106 private static final String TAG = NetworkMonitor.class.getSimpleName();
107 private static final boolean DBG = true;
108 private static final boolean VDBG = false;
109 private static final boolean VDBG_STALL = Log.isLoggable(TAG, Log.DEBUG);
Remi NGUYEN VANb85d8752019-01-30 23:39:24 +0900110 // TODO: use another permission for CaptivePortalLoginActivity once it has its own certificate
111 private static final String PERMISSION_NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS";
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900112 // Default configuration values for captive portal detection probes.
113 // TODO: append a random length parameter to the default HTTPS url.
114 // TODO: randomize browser version ids in the default User-Agent String.
115 private static final String DEFAULT_HTTPS_URL = "https://www.google.com/generate_204";
116 private static final String DEFAULT_FALLBACK_URL = "http://www.google.com/gen_204";
117 private static final String DEFAULT_OTHER_FALLBACK_URLS =
118 "http://play.googleapis.com/generate_204";
119 private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) "
120 + "AppleWebKit/537.36 (KHTML, like Gecko) "
121 + "Chrome/60.0.3112.32 Safari/537.36";
122
123 private static final int SOCKET_TIMEOUT_MS = 10000;
124 private static final int PROBE_TIMEOUT_MS = 3000;
125
126 // Default configuration values for data stall detection.
127 private static final int DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD = 5;
128 private static final int DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS = 60 * 1000;
129 private static final int DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS = 30 * 60 * 1000;
130
131 private static final int DATA_STALL_EVALUATION_TYPE_DNS = 1;
132 private static final int DEFAULT_DATA_STALL_EVALUATION_TYPES =
133 (1 << DATA_STALL_EVALUATION_TYPE_DNS);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800134 // Reevaluate it as intending to increase the number. Larger log size may cause statsd
135 // log buffer bust and have stats log lost.
136 private static final int DEFAULT_DNS_LOG_SIZE = 20;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900137
138 enum EvaluationResult {
139 VALIDATED(true),
140 CAPTIVE_PORTAL(false);
141 final boolean mIsValidated;
142 EvaluationResult(boolean isValidated) {
143 this.mIsValidated = isValidated;
144 }
145 }
146
147 enum ValidationStage {
148 FIRST_VALIDATION(true),
149 REVALIDATION(false);
150 final boolean mIsFirstValidation;
151 ValidationStage(boolean isFirstValidation) {
152 this.mIsFirstValidation = isFirstValidation;
153 }
154 }
155
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900156 /**
157 * ConnectivityService has sent a notification to indicate that network has connected.
158 * Initiates Network Validation.
159 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900160 private static final int CMD_NETWORK_CONNECTED = 1;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900161
162 /**
163 * Message to self indicating it's time to evaluate a network's connectivity.
164 * arg1 = Token to ignore old messages.
165 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900166 private static final int CMD_REEVALUATE = 6;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900167
168 /**
169 * ConnectivityService has sent a notification to indicate that network has disconnected.
170 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900171 private static final int CMD_NETWORK_DISCONNECTED = 7;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900172
173 /**
174 * Force evaluation even if it has succeeded in the past.
175 * arg1 = UID responsible for requesting this reeval. Will be billed for data.
176 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900177 private static final int CMD_FORCE_REEVALUATION = 8;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900178
179 /**
180 * Message to self indicating captive portal app finished.
181 * arg1 = one of: APP_RETURN_DISMISSED,
182 * APP_RETURN_UNWANTED,
183 * APP_RETURN_WANTED_AS_IS
184 * obj = mCaptivePortalLoggedInResponseToken as String
185 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900186 private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = 9;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900187
188 /**
189 * Message indicating sign-in app should be launched.
190 * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the
191 * user touches the sign in notification, or sent by
192 * ConnectivityService when the user touches the "sign into
193 * network" button in the wifi access point detail page.
194 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900195 private static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = 11;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900196
197 /**
198 * Retest network to see if captive portal is still in place.
199 * arg1 = UID responsible for requesting this reeval. Will be billed for data.
200 * 0 indicates self-initiated, so nobody to blame.
201 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900202 private static final int CMD_CAPTIVE_PORTAL_RECHECK = 12;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900203
204 /**
205 * ConnectivityService notifies NetworkMonitor of settings changes to
206 * Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
207 * strict mode, then an event is sent back to ConnectivityService with the
208 * result of the resolution attempt.
209 *
210 * A separate message is used to trigger (re)evaluation of the Private DNS
211 * configuration, so that the message can be handled as needed in different
212 * states, including being ignored until after an ongoing captive portal
213 * validation phase is completed.
214 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900215 private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = 13;
216 private static final int CMD_EVALUATE_PRIVATE_DNS = 15;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900217
218 /**
219 * Message to self indicating captive portal detection is completed.
220 * obj = CaptivePortalProbeResult for detection result;
221 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900222 public static final int CMD_PROBE_COMPLETE = 16;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900223
224 /**
225 * ConnectivityService notifies NetworkMonitor of DNS query responses event.
226 * arg1 = returncode in OnDnsEvent which indicates the response code for the DNS query.
227 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900228 public static final int EVENT_DNS_NOTIFICATION = 17;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900229
230 // Start mReevaluateDelayMs at this value and double.
231 private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
232 private static final int MAX_REEVALUATE_DELAY_MS = 10 * 60 * 1000;
233 // Before network has been evaluated this many times, ignore repeated reevaluate requests.
234 private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
235 private int mReevaluateToken = 0;
236 private static final int NO_UID = 0;
237 private static final int INVALID_UID = -1;
238 private int mUidResponsibleForReeval = INVALID_UID;
239 // Stop blaming UID that requested re-evaluation after this many attempts.
240 private static final int BLAME_FOR_EVALUATION_ATTEMPTS = 5;
241 // Delay between reevaluations once a captive portal has been found.
242 private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10 * 60 * 1000;
243
244 private String mPrivateDnsProviderHostname = "";
245
246 private final Context mContext;
247 private final INetworkMonitorCallbacks mCallback;
248 private final Network mNetwork;
249 private final Network mNonPrivateDnsBypassNetwork;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900250 private final TelephonyManager mTelephonyManager;
251 private final WifiManager mWifiManager;
252 private final ConnectivityManager mCm;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900253 private final IpConnectivityLog mMetricsLog;
254 private final Dependencies mDependencies;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800255 private final DataStallStatsUtils mDetectionStatsUtils;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900256
257 // Configuration values for captive portal detection probes.
258 private final String mCaptivePortalUserAgent;
259 private final URL mCaptivePortalHttpsUrl;
260 private final URL mCaptivePortalHttpUrl;
261 private final URL[] mCaptivePortalFallbackUrls;
262 @Nullable
263 private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs;
264
265 private NetworkCapabilities mNetworkCapabilities;
266 private LinkProperties mLinkProperties;
267
268 @VisibleForTesting
269 protected boolean mIsCaptivePortalCheckEnabled;
270
271 private boolean mUseHttps;
272 // The total number of captive portal detection attempts for this NetworkMonitor instance.
273 private int mValidations = 0;
274
275 // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
276 private boolean mUserDoesNotWant = false;
277 // Avoids surfacing "Sign in to network" notification.
278 private boolean mDontDisplaySigninNotification = false;
279
280 private volatile boolean mSystemReady = false;
281
282 private final State mDefaultState = new DefaultState();
283 private final State mValidatedState = new ValidatedState();
284 private final State mMaybeNotifyState = new MaybeNotifyState();
285 private final State mEvaluatingState = new EvaluatingState();
286 private final State mCaptivePortalState = new CaptivePortalState();
287 private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
288 private final State mProbingState = new ProbingState();
289 private final State mWaitingForNextProbeState = new WaitingForNextProbeState();
290
291 private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
292
293 private final SharedLog mValidationLogs;
294
295 private final Stopwatch mEvaluationTimer = new Stopwatch();
296
297 // This variable is set before transitioning to the mCaptivePortalState.
298 private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED;
299
300 // Random generator to select fallback URL index
301 private final Random mRandom;
302 private int mNextFallbackUrlIndex = 0;
303
304
305 private int mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
306 private int mEvaluateAttempts = 0;
307 private volatile int mProbeToken = 0;
308 private final int mConsecutiveDnsTimeoutThreshold;
309 private final int mDataStallMinEvaluateTime;
310 private final int mDataStallValidDnsTimeThreshold;
311 private final int mDataStallEvaluationType;
312 private final DnsStallDetector mDnsStallDetector;
313 private long mLastProbeTime;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800314 // Set to true if data stall is suspected and reset to false after metrics are sent to statsd.
315 private boolean mCollectDataStallMetrics = false;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900316
317 public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
Remi NGUYEN VAN6e9fdbd2019-01-29 15:38:52 +0900318 SharedLog validationLog) {
319 this(context, cb, network, new IpConnectivityLog(), validationLog,
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800320 Dependencies.DEFAULT, new DataStallStatsUtils());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900321 }
322
323 @VisibleForTesting
324 protected NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
Remi NGUYEN VAN6e9fdbd2019-01-29 15:38:52 +0900325 IpConnectivityLog logger, SharedLog validationLogs,
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800326 Dependencies deps, DataStallStatsUtils detectionStatsUtils) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900327 // Add suffix indicating which NetworkMonitor we're talking about.
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900328 super(TAG + "/" + network.toString());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900329
330 // Logs with a tag of the form given just above, e.g.
331 // <timestamp> 862 2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
332 setDbg(VDBG);
333
334 mContext = context;
335 mMetricsLog = logger;
336 mValidationLogs = validationLogs;
337 mCallback = cb;
338 mDependencies = deps;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800339 mDetectionStatsUtils = detectionStatsUtils;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900340 mNonPrivateDnsBypassNetwork = network;
341 mNetwork = deps.getPrivateDnsBypassNetwork(network);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900342 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
343 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
344 mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900345
346 // CHECKSTYLE:OFF IndentationCheck
347 addState(mDefaultState);
348 addState(mMaybeNotifyState, mDefaultState);
349 addState(mEvaluatingState, mMaybeNotifyState);
350 addState(mProbingState, mEvaluatingState);
351 addState(mWaitingForNextProbeState, mEvaluatingState);
352 addState(mCaptivePortalState, mMaybeNotifyState);
353 addState(mEvaluatingPrivateDnsState, mDefaultState);
354 addState(mValidatedState, mDefaultState);
355 setInitialState(mDefaultState);
356 // CHECKSTYLE:ON IndentationCheck
357
358 mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
359 mUseHttps = getUseHttpsValidation();
360 mCaptivePortalUserAgent = getCaptivePortalUserAgent();
361 mCaptivePortalHttpsUrl = makeURL(getCaptivePortalServerHttpsUrl());
362 mCaptivePortalHttpUrl = makeURL(deps.getCaptivePortalServerHttpUrl(context));
363 mCaptivePortalFallbackUrls = makeCaptivePortalFallbackUrls();
364 mCaptivePortalFallbackSpecs = makeCaptivePortalFallbackProbeSpecs();
365 mRandom = deps.getRandom();
366 // TODO: Evaluate to move data stall configuration to a specific class.
367 mConsecutiveDnsTimeoutThreshold = getConsecutiveDnsTimeoutThreshold();
368 mDnsStallDetector = new DnsStallDetector(mConsecutiveDnsTimeoutThreshold);
369 mDataStallMinEvaluateTime = getDataStallMinEvaluateTime();
370 mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold();
371 mDataStallEvaluationType = getDataStallEvalutionType();
372
373 // mLinkProperties and mNetworkCapbilities must never be null or we will NPE.
374 // Provide empty objects in case we are started and the network disconnects before
375 // we can ever fetch them.
376 // TODO: Delete ASAP.
377 mLinkProperties = new LinkProperties();
Remi NGUYEN VAN6e9fdbd2019-01-29 15:38:52 +0900378 mNetworkCapabilities = new NetworkCapabilities(null);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900379 }
380
381 /**
382 * Request the NetworkMonitor to reevaluate the network.
383 */
384 public void forceReevaluation(int responsibleUid) {
385 sendMessage(CMD_FORCE_REEVALUATION, responsibleUid, 0);
386 }
387
388 /**
389 * Send a notification to NetworkMonitor indicating that there was a DNS query response event.
390 * @param returnCode the DNS return code of the response.
391 */
392 public void notifyDnsResponse(int returnCode) {
393 sendMessage(EVENT_DNS_NOTIFICATION, returnCode);
394 }
395
396 /**
397 * Send a notification to NetworkMonitor indicating that private DNS settings have changed.
398 * @param newCfg The new private DNS configuration.
399 */
400 public void notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg) {
401 // Cancel any outstanding resolutions.
402 removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
403 // Send the update to the proper thread.
404 sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
405 }
406
407 /**
408 * Send a notification to NetworkMonitor indicating that the system is ready.
409 */
410 public void notifySystemReady() {
411 // No need to run on the handler thread: mSystemReady is volatile and read only once on the
412 // isCaptivePortal() thread.
413 mSystemReady = true;
414 }
415
416 /**
417 * Send a notification to NetworkMonitor indicating that the network is now connected.
418 */
419 public void notifyNetworkConnected() {
420 sendMessage(CMD_NETWORK_CONNECTED);
421 }
422
423 /**
424 * Send a notification to NetworkMonitor indicating that the network is now disconnected.
425 */
426 public void notifyNetworkDisconnected() {
427 sendMessage(CMD_NETWORK_DISCONNECTED);
428 }
429
430 /**
431 * Send a notification to NetworkMonitor indicating that link properties have changed.
432 */
433 public void notifyLinkPropertiesChanged() {
434 getHandler().post(() -> {
435 updateLinkProperties();
436 });
437 }
438
439 private void updateLinkProperties() {
440 final LinkProperties lp = mCm.getLinkProperties(mNetwork);
441 // If null, we should soon get a message that the network was disconnected, and will stop.
442 if (lp != null) {
443 // TODO: send LinkProperties parceled in notifyLinkPropertiesChanged() and start().
444 mLinkProperties = lp;
445 }
446 }
447
448 /**
449 * Send a notification to NetworkMonitor indicating that network capabilities have changed.
450 */
451 public void notifyNetworkCapabilitiesChanged() {
452 getHandler().post(() -> {
453 updateNetworkCapabilities();
454 });
455 }
456
457 private void updateNetworkCapabilities() {
458 final NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
459 // If null, we should soon get a message that the network was disconnected, and will stop.
460 if (nc != null) {
461 // TODO: send NetworkCapabilities parceled in notifyNetworkCapsChanged() and start().
462 mNetworkCapabilities = nc;
463 }
464 }
465
466 /**
467 * Request the captive portal application to be launched.
468 */
469 public void launchCaptivePortalApp() {
470 sendMessage(CMD_LAUNCH_CAPTIVE_PORTAL_APP);
471 }
472
Remi NGUYEN VANad99e542019-02-13 20:58:59 +0900473 /**
474 * Notify that the captive portal app was closed with the provided response code.
475 */
476 public void notifyCaptivePortalAppFinished(int response) {
477 sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response);
478 }
479
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900480 @Override
481 protected void log(String s) {
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900482 if (DBG) Log.d(TAG + "/" + mNetwork.toString(), s);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900483 }
484
485 private void validationLog(int probeType, Object url, String msg) {
486 String probeName = ValidationProbeEvent.getProbeName(probeType);
487 validationLog(String.format("%s %s %s", probeName, url, msg));
488 }
489
490 private void validationLog(String s) {
491 if (DBG) log(s);
492 mValidationLogs.log(s);
493 }
494
495 private ValidationStage validationStage() {
496 return 0 == mValidations ? ValidationStage.FIRST_VALIDATION : ValidationStage.REVALIDATION;
497 }
498
499 private boolean isValidationRequired() {
Lorenzo Colittid4476892019-01-23 17:54:08 +0900500 return NetworkMonitorUtils.isValidationRequired(mNetworkCapabilities);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900501 }
502
503
504 private void notifyNetworkTested(int result, @Nullable String redirectUrl) {
505 try {
506 mCallback.notifyNetworkTested(result, redirectUrl);
507 } catch (RemoteException e) {
508 Log.e(TAG, "Error sending network test result", e);
509 }
510 }
511
512 private void showProvisioningNotification(String action) {
513 try {
Remi NGUYEN VAN1e3eb372019-02-07 21:29:57 +0900514 mCallback.showProvisioningNotification(action, mContext.getPackageName());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900515 } catch (RemoteException e) {
516 Log.e(TAG, "Error showing provisioning notification", e);
517 }
518 }
519
520 private void hideProvisioningNotification() {
521 try {
522 mCallback.hideProvisioningNotification();
523 } catch (RemoteException e) {
524 Log.e(TAG, "Error hiding provisioning notification", e);
525 }
526 }
527
528 // DefaultState is the parent of all States. It exists only to handle CMD_* messages but
529 // does not entail any real state (hence no enter() or exit() routines).
530 private class DefaultState extends State {
531 @Override
532 public void enter() {
533 // TODO: have those passed parceled in start() and remove this
534 updateLinkProperties();
535 updateNetworkCapabilities();
536 }
537
538 @Override
539 public boolean processMessage(Message message) {
540 switch (message.what) {
541 case CMD_NETWORK_CONNECTED:
542 logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
543 transitionTo(mEvaluatingState);
544 return HANDLED;
545 case CMD_NETWORK_DISCONNECTED:
546 logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
547 if (mLaunchCaptivePortalAppBroadcastReceiver != null) {
548 mContext.unregisterReceiver(mLaunchCaptivePortalAppBroadcastReceiver);
549 mLaunchCaptivePortalAppBroadcastReceiver = null;
550 }
551 quit();
552 return HANDLED;
553 case CMD_FORCE_REEVALUATION:
554 case CMD_CAPTIVE_PORTAL_RECHECK:
Chiachang Wang0a880da2019-01-15 10:32:48 +0800555 final int dnsCount = mDnsStallDetector.getConsecutiveTimeoutCount();
556 validationLog("Forcing reevaluation for UID " + message.arg1
557 + ". Dns signal count: " + dnsCount);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900558 mUidResponsibleForReeval = message.arg1;
559 transitionTo(mEvaluatingState);
560 return HANDLED;
561 case CMD_CAPTIVE_PORTAL_APP_FINISHED:
562 log("CaptivePortal App responded with " + message.arg1);
563
564 // If the user has seen and acted on a captive portal notification, and the
565 // captive portal app is now closed, disable HTTPS probes. This avoids the
566 // following pathological situation:
567 //
568 // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out.
569 // 2. User opens the app and logs into the captive portal.
570 // 3. HTTP starts working, but HTTPS still doesn't work for some other reason -
571 // perhaps due to the network blocking HTTPS?
572 //
573 // In this case, we'll fail to validate the network even after the app is
574 // dismissed. There is now no way to use this network, because the app is now
575 // gone, so the user cannot select "Use this network as is".
576 mUseHttps = false;
577
578 switch (message.arg1) {
579 case APP_RETURN_DISMISSED:
580 sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
581 break;
582 case APP_RETURN_WANTED_AS_IS:
583 mDontDisplaySigninNotification = true;
584 // TODO: Distinguish this from a network that actually validates.
585 // Displaying the "x" on the system UI icon may still be a good idea.
586 transitionTo(mEvaluatingPrivateDnsState);
587 break;
588 case APP_RETURN_UNWANTED:
589 mDontDisplaySigninNotification = true;
590 mUserDoesNotWant = true;
591 notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null);
592 // TODO: Should teardown network.
593 mUidResponsibleForReeval = 0;
594 transitionTo(mEvaluatingState);
595 break;
596 }
597 return HANDLED;
598 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
599 final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
600 if (!isValidationRequired() || cfg == null || !cfg.inStrictMode()) {
601 // No DNS resolution required.
602 //
603 // We don't force any validation in opportunistic mode
604 // here. Opportunistic mode nameservers are validated
605 // separately within netd.
606 //
607 // Reset Private DNS settings state.
608 mPrivateDnsProviderHostname = "";
609 break;
610 }
611
612 mPrivateDnsProviderHostname = cfg.hostname;
613
614 // DNS resolutions via Private DNS strict mode block for a
615 // few seconds (~4.2) checking for any IP addresses to
616 // arrive and validate. Initiating a (re)evaluation now
617 // should not significantly alter the validation outcome.
618 //
619 // No matter what: enqueue a validation request; one of
620 // three things can happen with this request:
621 // [1] ignored (EvaluatingState or CaptivePortalState)
622 // [2] transition to EvaluatingPrivateDnsState
623 // (DefaultState and ValidatedState)
624 // [3] handled (EvaluatingPrivateDnsState)
625 //
626 // The Private DNS configuration to be evaluated will:
627 // [1] be skipped (not in strict mode), or
628 // [2] validate (huzzah), or
629 // [3] encounter some problem (invalid hostname,
630 // no resolved IP addresses, IPs unreachable,
631 // port 853 unreachable, port 853 is not running a
632 // DNS-over-TLS server, et cetera).
633 sendMessage(CMD_EVALUATE_PRIVATE_DNS);
634 break;
635 }
636 case EVENT_DNS_NOTIFICATION:
637 mDnsStallDetector.accumulateConsecutiveDnsTimeoutCount(message.arg1);
638 break;
639 default:
640 break;
641 }
642 return HANDLED;
643 }
644 }
645
646 // Being in the ValidatedState State indicates a Network is:
647 // - Successfully validated, or
648 // - Wanted "as is" by the user, or
649 // - Does not satisfy the default NetworkRequest and so validation has been skipped.
650 private class ValidatedState extends State {
651 @Override
652 public void enter() {
653 maybeLogEvaluationResult(
654 networkEventType(validationStage(), EvaluationResult.VALIDATED));
655 notifyNetworkTested(INetworkMonitor.NETWORK_TEST_RESULT_VALID, null);
656 mValidations++;
657 }
658
659 @Override
660 public boolean processMessage(Message message) {
661 switch (message.what) {
662 case CMD_NETWORK_CONNECTED:
663 transitionTo(mValidatedState);
664 break;
665 case CMD_EVALUATE_PRIVATE_DNS:
666 transitionTo(mEvaluatingPrivateDnsState);
667 break;
668 case EVENT_DNS_NOTIFICATION:
669 mDnsStallDetector.accumulateConsecutiveDnsTimeoutCount(message.arg1);
670 if (isDataStall()) {
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800671 mCollectDataStallMetrics = true;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900672 validationLog("Suspecting data stall, reevaluate");
673 transitionTo(mEvaluatingState);
674 }
675 break;
676 default:
677 return NOT_HANDLED;
678 }
679 return HANDLED;
680 }
681 }
682
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800683 private void writeDataStallStats(@NonNull final CaptivePortalProbeResult result) {
684 /*
685 * Collect data stall detection level information for each transport type. Collect type
686 * specific information for cellular and wifi only currently. Generate
687 * DataStallDetectionStats for each transport type. E.g., if a network supports both
688 * TRANSPORT_WIFI and TRANSPORT_VPN, two DataStallDetectionStats will be generated.
689 */
690 final int[] transports = mNetworkCapabilities.getTransportTypes();
691
692 for (int i = 0; i < transports.length; i++) {
693 DataStallStatsUtils.write(buildDataStallDetectionStats(transports[i]), result);
694 }
695 mCollectDataStallMetrics = false;
696 }
697
698 @VisibleForTesting
699 protected DataStallDetectionStats buildDataStallDetectionStats(int transport) {
700 final DataStallDetectionStats.Builder stats = new DataStallDetectionStats.Builder();
701 if (VDBG_STALL) log("collectDataStallMetrics: type=" + transport);
702 stats.setEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS);
703 stats.setNetworkType(transport);
704 switch (transport) {
705 case NetworkCapabilities.TRANSPORT_WIFI:
706 // TODO: Update it if status query in dual wifi is supported.
707 final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
708 stats.setWiFiData(wifiInfo);
709 break;
710 case NetworkCapabilities.TRANSPORT_CELLULAR:
711 final boolean isRoaming = !mNetworkCapabilities.hasCapability(
712 NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
713 final SignalStrength ss = mTelephonyManager.getSignalStrength();
714 // TODO(b/120452078): Support multi-sim.
715 stats.setCellData(
716 mTelephonyManager.getDataNetworkType(),
717 isRoaming,
718 mTelephonyManager.getNetworkOperator(),
719 mTelephonyManager.getSimOperator(),
720 (ss != null)
721 ? ss.getLevel() : CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
722 break;
723 default:
724 // No transport type specific information for the other types.
725 break;
726 }
727 addDnsEvents(stats);
728
729 return stats.build();
730 }
731
732 private void addDnsEvents(@NonNull final DataStallDetectionStats.Builder stats) {
733 final int size = mDnsStallDetector.mResultIndices.size();
734 for (int i = 1; i <= DEFAULT_DNS_LOG_SIZE && i <= size; i++) {
735 final int index = mDnsStallDetector.mResultIndices.indexOf(size - i);
736 stats.addDnsEvent(mDnsStallDetector.mDnsEvents[index].mReturnCode,
737 mDnsStallDetector.mDnsEvents[index].mTimeStamp);
738 }
739 }
740
741
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900742 // Being in the MaybeNotifyState State indicates the user may have been notified that sign-in
743 // is required. This State takes care to clear the notification upon exit from the State.
744 private class MaybeNotifyState extends State {
745 @Override
746 public boolean processMessage(Message message) {
747 switch (message.what) {
748 case CMD_LAUNCH_CAPTIVE_PORTAL_APP:
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +0900749 final Bundle appExtras = new Bundle();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900750 // OneAddressPerFamilyNetwork is not parcelable across processes.
Remi NGUYEN VANad99e542019-02-13 20:58:59 +0900751 final Network network = new Network(mNetwork);
752 appExtras.putParcelable(ConnectivityManager.EXTRA_NETWORK, network);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900753 final CaptivePortalProbeResult probeRes = mLastPortalProbeResult;
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +0900754 appExtras.putString(EXTRA_CAPTIVE_PORTAL_URL, probeRes.detectUrl);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900755 if (probeRes.probeSpec != null) {
756 final String encodedSpec = probeRes.probeSpec.getEncodedSpec();
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +0900757 appExtras.putString(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC, encodedSpec);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900758 }
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +0900759 appExtras.putString(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900760 mCaptivePortalUserAgent);
Remi NGUYEN VANad99e542019-02-13 20:58:59 +0900761 mCm.startCaptivePortalApp(network, appExtras);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900762 return HANDLED;
763 default:
764 return NOT_HANDLED;
765 }
766 }
767
768 @Override
769 public void exit() {
770 hideProvisioningNotification();
771 }
772 }
773
774 // Being in the EvaluatingState State indicates the Network is being evaluated for internet
775 // connectivity, or that the user has indicated that this network is unwanted.
776 private class EvaluatingState extends State {
777 @Override
778 public void enter() {
779 // If we have already started to track time spent in EvaluatingState
780 // don't reset the timer due simply to, say, commands or events that
781 // cause us to exit and re-enter EvaluatingState.
782 if (!mEvaluationTimer.isStarted()) {
783 mEvaluationTimer.start();
784 }
785 sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
786 if (mUidResponsibleForReeval != INVALID_UID) {
787 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
788 mUidResponsibleForReeval = INVALID_UID;
789 }
790 mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
791 mEvaluateAttempts = 0;
792 }
793
794 @Override
795 public boolean processMessage(Message message) {
796 switch (message.what) {
797 case CMD_REEVALUATE:
798 if (message.arg1 != mReevaluateToken || mUserDoesNotWant) {
799 return HANDLED;
800 }
801 // Don't bother validating networks that don't satisfy the default request.
802 // This includes:
803 // - VPNs which can be considered explicitly desired by the user and the
804 // user's desire trumps whether the network validates.
805 // - Networks that don't provide Internet access. It's unclear how to
806 // validate such networks.
807 // - Untrusted networks. It's unsafe to prompt the user to sign-in to
808 // such networks and the user didn't express interest in connecting to
809 // such networks (an app did) so the user may be unhappily surprised when
810 // asked to sign-in to a network they didn't want to connect to in the
811 // first place. Validation could be done to adjust the network scores
812 // however these networks are app-requested and may not be intended for
813 // general usage, in which case general validation may not be an accurate
814 // measure of the network's quality. Only the app knows how to evaluate
815 // the network so don't bother validating here. Furthermore sending HTTP
816 // packets over the network may be undesirable, for example an extremely
817 // expensive metered network, or unwanted leaking of the User Agent string.
818 if (!isValidationRequired()) {
819 validationLog("Network would not satisfy default request, not validating");
820 transitionTo(mValidatedState);
821 return HANDLED;
822 }
823 mEvaluateAttempts++;
824
825 transitionTo(mProbingState);
826 return HANDLED;
827 case CMD_FORCE_REEVALUATION:
828 // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made,
829 // ignore any re-evaluation requests. After, restart the
830 // evaluation process via EvaluatingState#enter.
831 return (mEvaluateAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED;
832 default:
833 return NOT_HANDLED;
834 }
835 }
836
837 @Override
838 public void exit() {
839 TrafficStats.clearThreadStatsUid();
840 }
841 }
842
843 // BroadcastReceiver that waits for a particular Intent and then posts a message.
844 private class CustomIntentReceiver extends BroadcastReceiver {
845 private final int mToken;
846 private final int mWhat;
847 private final String mAction;
848 CustomIntentReceiver(String action, int token, int what) {
849 mToken = token;
850 mWhat = what;
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900851 mAction = action + "_" + mNetwork.getNetworkHandle() + "_" + token;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900852 mContext.registerReceiver(this, new IntentFilter(mAction));
853 }
854 public PendingIntent getPendingIntent() {
855 final Intent intent = new Intent(mAction);
856 intent.setPackage(mContext.getPackageName());
857 return PendingIntent.getBroadcast(mContext, 0, intent, 0);
858 }
859 @Override
860 public void onReceive(Context context, Intent intent) {
861 if (intent.getAction().equals(mAction)) sendMessage(obtainMessage(mWhat, mToken));
862 }
863 }
864
865 // Being in the CaptivePortalState State indicates a captive portal was detected and the user
866 // has been shown a notification to sign-in.
867 private class CaptivePortalState extends State {
868 private static final String ACTION_LAUNCH_CAPTIVE_PORTAL_APP =
869 "android.net.netmon.launchCaptivePortalApp";
870
871 @Override
872 public void enter() {
873 maybeLogEvaluationResult(
874 networkEventType(validationStage(), EvaluationResult.CAPTIVE_PORTAL));
875 // Don't annoy user with sign-in notifications.
876 if (mDontDisplaySigninNotification) return;
877 // Create a CustomIntentReceiver that sends us a
878 // CMD_LAUNCH_CAPTIVE_PORTAL_APP message when the user
879 // touches the notification.
880 if (mLaunchCaptivePortalAppBroadcastReceiver == null) {
881 // Wait for result.
882 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver(
883 ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(),
884 CMD_LAUNCH_CAPTIVE_PORTAL_APP);
885 }
886 // Display the sign in notification.
887 showProvisioningNotification(mLaunchCaptivePortalAppBroadcastReceiver.mAction);
888 // Retest for captive portal occasionally.
889 sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
890 CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
891 mValidations++;
892 }
893
894 @Override
895 public void exit() {
896 removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
897 }
898 }
899
900 private class EvaluatingPrivateDnsState extends State {
901 private int mPrivateDnsReevalDelayMs;
902 private PrivateDnsConfig mPrivateDnsConfig;
903
904 @Override
905 public void enter() {
906 mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
907 mPrivateDnsConfig = null;
908 sendMessage(CMD_EVALUATE_PRIVATE_DNS);
909 }
910
911 @Override
912 public boolean processMessage(Message msg) {
913 switch (msg.what) {
914 case CMD_EVALUATE_PRIVATE_DNS:
915 if (inStrictMode()) {
916 if (!isStrictModeHostnameResolved()) {
917 resolveStrictModeHostname();
918
919 if (isStrictModeHostnameResolved()) {
920 notifyPrivateDnsConfigResolved();
921 } else {
922 handlePrivateDnsEvaluationFailure();
923 break;
924 }
925 }
926
927 // Look up a one-time hostname, to bypass caching.
928 //
929 // Note that this will race with ConnectivityService
930 // code programming the DNS-over-TLS server IP addresses
931 // into netd (if invoked, above). If netd doesn't know
932 // the IP addresses yet, or if the connections to the IP
933 // addresses haven't yet been validated, netd will block
934 // for up to a few seconds before failing the lookup.
935 if (!sendPrivateDnsProbe()) {
936 handlePrivateDnsEvaluationFailure();
937 break;
938 }
939 }
940
941 // All good!
942 transitionTo(mValidatedState);
943 break;
944 default:
945 return NOT_HANDLED;
946 }
947 return HANDLED;
948 }
949
950 private boolean inStrictMode() {
951 return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
952 }
953
954 private boolean isStrictModeHostnameResolved() {
955 return (mPrivateDnsConfig != null)
956 && mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname)
957 && (mPrivateDnsConfig.ips.length > 0);
958 }
959
960 private void resolveStrictModeHostname() {
961 try {
962 // Do a blocking DNS resolution using the network-assigned nameservers.
963 final InetAddress[] ips = mNetwork.getAllByName(mPrivateDnsProviderHostname);
964 mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
965 validationLog("Strict mode hostname resolved: " + mPrivateDnsConfig);
966 } catch (UnknownHostException uhe) {
967 mPrivateDnsConfig = null;
968 validationLog("Strict mode hostname resolution failed: " + uhe.getMessage());
969 }
970 }
971
972 private void notifyPrivateDnsConfigResolved() {
973 try {
974 mCallback.notifyPrivateDnsConfigResolved(mPrivateDnsConfig.toParcel());
975 } catch (RemoteException e) {
976 Log.e(TAG, "Error sending private DNS config resolved notification", e);
977 }
978 }
979
980 private void handlePrivateDnsEvaluationFailure() {
981 notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null);
982
983 // Queue up a re-evaluation with backoff.
984 //
985 // TODO: Consider abandoning this state after a few attempts and
986 // transitioning back to EvaluatingState, to perhaps give ourselves
987 // the opportunity to (re)detect a captive portal or something.
988 sendMessageDelayed(CMD_EVALUATE_PRIVATE_DNS, mPrivateDnsReevalDelayMs);
989 mPrivateDnsReevalDelayMs *= 2;
990 if (mPrivateDnsReevalDelayMs > MAX_REEVALUATE_DELAY_MS) {
991 mPrivateDnsReevalDelayMs = MAX_REEVALUATE_DELAY_MS;
992 }
993 }
994
995 private boolean sendPrivateDnsProbe() {
996 // q.v. system/netd/server/dns/DnsTlsTransport.cpp
997 final String oneTimeHostnameSuffix = "-dnsotls-ds.metric.gstatic.com";
998 final String host = UUID.randomUUID().toString().substring(0, 8)
999 + oneTimeHostnameSuffix;
1000 final Stopwatch watch = new Stopwatch().start();
1001 try {
1002 final InetAddress[] ips = mNonPrivateDnsBypassNetwork.getAllByName(host);
1003 final long time = watch.stop();
1004 final String strIps = Arrays.toString(ips);
1005 final boolean success = (ips != null && ips.length > 0);
1006 validationLog(PROBE_PRIVDNS, host, String.format("%dms: %s", time, strIps));
1007 logValidationProbe(time, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE);
1008 return success;
1009 } catch (UnknownHostException uhe) {
1010 final long time = watch.stop();
1011 validationLog(PROBE_PRIVDNS, host,
1012 String.format("%dms - Error: %s", time, uhe.getMessage()));
1013 logValidationProbe(time, PROBE_PRIVDNS, DNS_FAILURE);
1014 }
1015 return false;
1016 }
1017 }
1018
1019 private class ProbingState extends State {
1020 private Thread mThread;
1021
1022 @Override
1023 public void enter() {
1024 if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
1025 //Don't continue to blame UID forever.
1026 TrafficStats.clearThreadStatsUid();
1027 }
1028
1029 final int token = ++mProbeToken;
1030 mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
1031 isCaptivePortal())));
1032 mThread.start();
1033 }
1034
1035 @Override
1036 public boolean processMessage(Message message) {
1037 switch (message.what) {
1038 case CMD_PROBE_COMPLETE:
1039 // Ensure that CMD_PROBE_COMPLETE from stale threads are ignored.
1040 if (message.arg1 != mProbeToken) {
1041 return HANDLED;
1042 }
1043
1044 final CaptivePortalProbeResult probeResult =
1045 (CaptivePortalProbeResult) message.obj;
1046 mLastProbeTime = SystemClock.elapsedRealtime();
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001047
1048 if (mCollectDataStallMetrics) {
1049 writeDataStallStats(probeResult);
1050 }
1051
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001052 if (probeResult.isSuccessful()) {
1053 // Transit EvaluatingPrivateDnsState to get to Validated
1054 // state (even if no Private DNS validation required).
1055 transitionTo(mEvaluatingPrivateDnsState);
1056 } else if (probeResult.isPortal()) {
1057 notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, probeResult.redirectUrl);
1058 mLastPortalProbeResult = probeResult;
1059 transitionTo(mCaptivePortalState);
1060 } else {
1061 logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
1062 notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, probeResult.redirectUrl);
1063 transitionTo(mWaitingForNextProbeState);
1064 }
1065 return HANDLED;
1066 case EVENT_DNS_NOTIFICATION:
1067 // Leave the event to DefaultState to record correct dns timestamp.
1068 return NOT_HANDLED;
1069 default:
1070 // Wait for probe result and defer events to next state by default.
1071 deferMessage(message);
1072 return HANDLED;
1073 }
1074 }
1075
1076 @Override
1077 public void exit() {
1078 if (mThread.isAlive()) {
1079 mThread.interrupt();
1080 }
1081 mThread = null;
1082 }
1083 }
1084
1085 // Being in the WaitingForNextProbeState indicates that evaluating probes failed and state is
1086 // transited from ProbingState. This ensures that the state machine is only in ProbingState
1087 // while a probe is in progress, not while waiting to perform the next probe. That allows
1088 // ProbingState to defer most messages until the probe is complete, which keeps the code simple
1089 // and matches the pre-Q behaviour where probes were a blocking operation performed on the state
1090 // machine thread.
1091 private class WaitingForNextProbeState extends State {
1092 @Override
1093 public void enter() {
1094 scheduleNextProbe();
1095 }
1096
1097 private void scheduleNextProbe() {
1098 final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
1099 sendMessageDelayed(msg, mReevaluateDelayMs);
1100 mReevaluateDelayMs *= 2;
1101 if (mReevaluateDelayMs > MAX_REEVALUATE_DELAY_MS) {
1102 mReevaluateDelayMs = MAX_REEVALUATE_DELAY_MS;
1103 }
1104 }
1105
1106 @Override
1107 public boolean processMessage(Message message) {
1108 return NOT_HANDLED;
1109 }
1110 }
1111
1112 // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
1113 // most one per address family. This ensures we only wait up to 20 seconds for TCP connections
1114 // to complete, regardless of how many IP addresses a host has.
1115 private static class OneAddressPerFamilyNetwork extends Network {
1116 OneAddressPerFamilyNetwork(Network network) {
1117 // Always bypass Private DNS.
1118 super(network.getPrivateDnsBypassingCopy());
1119 }
1120
1121 @Override
1122 public InetAddress[] getAllByName(String host) throws UnknownHostException {
1123 final List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
1124
1125 // Ensure the address family of the first address is tried first.
1126 LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
1127 addressByFamily.put(addrs.get(0).getClass(), addrs.get(0));
1128 Collections.shuffle(addrs);
1129
1130 for (InetAddress addr : addrs) {
1131 addressByFamily.put(addr.getClass(), addr);
1132 }
1133
1134 return addressByFamily.values().toArray(new InetAddress[addressByFamily.size()]);
1135 }
1136 }
1137
1138 private boolean getIsCaptivePortalCheckEnabled() {
1139 String symbol = Settings.Global.CAPTIVE_PORTAL_MODE;
1140 int defaultValue = Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT;
1141 int mode = mDependencies.getSetting(mContext, symbol, defaultValue);
1142 return mode != Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE;
1143 }
1144
1145 private boolean getUseHttpsValidation() {
1146 return mDependencies.getSetting(mContext, Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
1147 }
1148
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001149 private String getCaptivePortalServerHttpsUrl() {
1150 return mDependencies.getSetting(mContext,
1151 Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
1152 }
1153
1154 private int getConsecutiveDnsTimeoutThreshold() {
1155 return mDependencies.getSetting(mContext,
1156 Settings.Global.DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD,
1157 DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD);
1158 }
1159
1160 private int getDataStallMinEvaluateTime() {
1161 return mDependencies.getSetting(mContext,
1162 Settings.Global.DATA_STALL_MIN_EVALUATE_INTERVAL,
1163 DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS);
1164 }
1165
1166 private int getDataStallValidDnsTimeThreshold() {
1167 return mDependencies.getSetting(mContext,
1168 Settings.Global.DATA_STALL_VALID_DNS_TIME_THRESHOLD,
1169 DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS);
1170 }
1171
1172 private int getDataStallEvalutionType() {
1173 return mDependencies.getSetting(mContext, Settings.Global.DATA_STALL_EVALUATION_TYPE,
1174 DEFAULT_DATA_STALL_EVALUATION_TYPES);
1175 }
1176
1177 private URL[] makeCaptivePortalFallbackUrls() {
1178 try {
1179 String separator = ",";
1180 String firstUrl = mDependencies.getSetting(mContext,
1181 Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL);
1182 String joinedUrls = firstUrl + separator + mDependencies.getSetting(mContext,
1183 Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS,
1184 DEFAULT_OTHER_FALLBACK_URLS);
1185 List<URL> urls = new ArrayList<>();
1186 for (String s : joinedUrls.split(separator)) {
1187 URL u = makeURL(s);
1188 if (u == null) {
1189 continue;
1190 }
1191 urls.add(u);
1192 }
1193 if (urls.isEmpty()) {
1194 Log.e(TAG, String.format("could not create any url from %s", joinedUrls));
1195 }
1196 return urls.toArray(new URL[urls.size()]);
1197 } catch (Exception e) {
1198 // Don't let a misconfiguration bootloop the system.
1199 Log.e(TAG, "Error parsing configured fallback URLs", e);
1200 return new URL[0];
1201 }
1202 }
1203
1204 private CaptivePortalProbeSpec[] makeCaptivePortalFallbackProbeSpecs() {
1205 try {
1206 final String settingsValue = mDependencies.getSetting(
1207 mContext, Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS, null);
1208 // Probe specs only used if configured in settings
1209 if (TextUtils.isEmpty(settingsValue)) {
1210 return null;
1211 }
1212
Remi NGUYEN VAN777022e2019-01-28 13:28:35 +09001213 final Collection<CaptivePortalProbeSpec> specs =
1214 CaptivePortalProbeSpec.parseCaptivePortalProbeSpecs(settingsValue);
1215 final CaptivePortalProbeSpec[] specsArray = new CaptivePortalProbeSpec[specs.size()];
1216 return specs.toArray(specsArray);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001217 } catch (Exception e) {
1218 // Don't let a misconfiguration bootloop the system.
1219 Log.e(TAG, "Error parsing configured fallback probe specs", e);
1220 return null;
1221 }
1222 }
1223
1224 private String getCaptivePortalUserAgent() {
1225 return mDependencies.getSetting(mContext,
1226 Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
1227 }
1228
1229 private URL nextFallbackUrl() {
1230 if (mCaptivePortalFallbackUrls.length == 0) {
1231 return null;
1232 }
1233 int idx = Math.abs(mNextFallbackUrlIndex) % mCaptivePortalFallbackUrls.length;
1234 mNextFallbackUrlIndex += mRandom.nextInt(); // randomly change url without memory.
1235 return mCaptivePortalFallbackUrls[idx];
1236 }
1237
1238 private CaptivePortalProbeSpec nextFallbackSpec() {
Remi NGUYEN VANabeaaf72019-01-20 13:48:19 +09001239 if (isEmpty(mCaptivePortalFallbackSpecs)) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001240 return null;
1241 }
1242 // Randomly change spec without memory. Also randomize the first attempt.
1243 final int idx = Math.abs(mRandom.nextInt()) % mCaptivePortalFallbackSpecs.length;
1244 return mCaptivePortalFallbackSpecs[idx];
1245 }
1246
1247 @VisibleForTesting
1248 protected CaptivePortalProbeResult isCaptivePortal() {
1249 if (!mIsCaptivePortalCheckEnabled) {
1250 validationLog("Validation disabled.");
1251 return CaptivePortalProbeResult.SUCCESS;
1252 }
1253
1254 URL pacUrl = null;
1255 URL httpsUrl = mCaptivePortalHttpsUrl;
1256 URL httpUrl = mCaptivePortalHttpUrl;
1257
1258 // On networks with a PAC instead of fetching a URL that should result in a 204
1259 // response, we instead simply fetch the PAC script. This is done for a few reasons:
1260 // 1. At present our PAC code does not yet handle multiple PACs on multiple networks
1261 // until something like https://android-review.googlesource.com/#/c/115180/ lands.
1262 // Network.openConnection() will ignore network-specific PACs and instead fetch
1263 // using NO_PROXY. If a PAC is in place, the only fetch we know will succeed with
1264 // NO_PROXY is the fetch of the PAC itself.
1265 // 2. To proxy the generate_204 fetch through a PAC would require a number of things
1266 // happen before the fetch can commence, namely:
1267 // a) the PAC script be fetched
1268 // b) a PAC script resolver service be fired up and resolve the captive portal
1269 // server.
1270 // Network validation could be delayed until these prerequisities are satisifed or
1271 // could simply be left to race them. Neither is an optimal solution.
1272 // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
1273 // fact block fetching of the generate_204 URL which would lead to false negative
1274 // results for network validation.
1275 final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
1276 if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
1277 pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
1278 if (pacUrl == null) {
1279 return CaptivePortalProbeResult.FAILED;
1280 }
1281 }
1282
1283 if ((pacUrl == null) && (httpUrl == null || httpsUrl == null)) {
1284 return CaptivePortalProbeResult.FAILED;
1285 }
1286
1287 long startTime = SystemClock.elapsedRealtime();
1288
1289 final CaptivePortalProbeResult result;
1290 if (pacUrl != null) {
1291 result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
1292 } else if (mUseHttps) {
1293 result = sendParallelHttpProbes(proxyInfo, httpsUrl, httpUrl);
1294 } else {
1295 result = sendDnsAndHttpProbes(proxyInfo, httpUrl, ValidationProbeEvent.PROBE_HTTP);
1296 }
1297
1298 long endTime = SystemClock.elapsedRealtime();
1299
1300 sendNetworkConditionsBroadcast(true /* response received */,
1301 result.isPortal() /* isCaptivePortal */,
1302 startTime, endTime);
1303
1304 log("isCaptivePortal: isSuccessful()=" + result.isSuccessful()
1305 + " isPortal()=" + result.isPortal()
1306 + " RedirectUrl=" + result.redirectUrl
1307 + " Time=" + (endTime - startTime) + "ms");
1308
1309 return result;
1310 }
1311
1312 /**
1313 * Do a DNS resolution and URL fetch on a known web server to see if we get the data we expect.
1314 * @return a CaptivePortalProbeResult inferred from the HTTP response.
1315 */
1316 private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
1317 // Pre-resolve the captive portal server host so we can log it.
1318 // Only do this if HttpURLConnection is about to, to avoid any potentially
1319 // unnecessary resolution.
1320 final String host = (proxy != null) ? proxy.getHost() : url.getHost();
1321 sendDnsProbe(host);
1322 return sendHttpProbe(url, probeType, null);
1323 }
1324
1325 /** Do a DNS resolution of the given server. */
1326 private void sendDnsProbe(String host) {
1327 if (TextUtils.isEmpty(host)) {
1328 return;
1329 }
1330
1331 final String name = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
1332 final Stopwatch watch = new Stopwatch().start();
1333 int result;
1334 String connectInfo;
1335 try {
1336 InetAddress[] addresses = mNetwork.getAllByName(host);
1337 StringBuffer buffer = new StringBuffer();
1338 for (InetAddress address : addresses) {
1339 buffer.append(',').append(address.getHostAddress());
1340 }
1341 result = ValidationProbeEvent.DNS_SUCCESS;
1342 connectInfo = "OK " + buffer.substring(1);
1343 } catch (UnknownHostException e) {
1344 result = ValidationProbeEvent.DNS_FAILURE;
1345 connectInfo = "FAIL";
1346 }
1347 final long latency = watch.stop();
1348 validationLog(ValidationProbeEvent.PROBE_DNS, host,
1349 String.format("%dms %s", latency, connectInfo));
1350 logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result);
1351 }
1352
1353 /**
1354 * Do a URL fetch on a known web server to see if we get the data we expect.
1355 * @return a CaptivePortalProbeResult inferred from the HTTP response.
1356 */
1357 @VisibleForTesting
1358 protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
1359 @Nullable CaptivePortalProbeSpec probeSpec) {
1360 HttpURLConnection urlConnection = null;
1361 int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
1362 String redirectUrl = null;
1363 final Stopwatch probeTimer = new Stopwatch().start();
1364 final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_PROBE);
1365 try {
1366 urlConnection = (HttpURLConnection) mNetwork.openConnection(url);
1367 urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
1368 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
1369 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
Hongshikfc092a82019-01-12 03:09:34 +09001370 urlConnection.setRequestProperty("Connection", "close");
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001371 urlConnection.setUseCaches(false);
1372 if (mCaptivePortalUserAgent != null) {
1373 urlConnection.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
1374 }
1375 // cannot read request header after connection
1376 String requestHeader = urlConnection.getRequestProperties().toString();
1377
1378 // Time how long it takes to get a response to our request
1379 long requestTimestamp = SystemClock.elapsedRealtime();
1380
1381 httpResponseCode = urlConnection.getResponseCode();
1382 redirectUrl = urlConnection.getHeaderField("location");
1383
1384 // Time how long it takes to get a response to our request
1385 long responseTimestamp = SystemClock.elapsedRealtime();
1386
1387 validationLog(probeType, url, "time=" + (responseTimestamp - requestTimestamp) + "ms"
1388 + " ret=" + httpResponseCode
1389 + " request=" + requestHeader
1390 + " headers=" + urlConnection.getHeaderFields());
1391 // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
1392 // portal. The only example of this seen so far was a captive portal. For
1393 // the time being go with prior behavior of assuming it's not a captive
1394 // portal. If it is considered a captive portal, a different sign-in URL
1395 // is needed (i.e. can't browse a 204). This could be the result of an HTTP
1396 // proxy server.
1397 if (httpResponseCode == 200) {
1398 if (probeType == ValidationProbeEvent.PROBE_PAC) {
1399 validationLog(
1400 probeType, url, "PAC fetch 200 response interpreted as 204 response.");
1401 httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1402 } else if (urlConnection.getContentLengthLong() == 0) {
1403 // Consider 200 response with "Content-length=0" to not be a captive portal.
1404 // There's no point in considering this a captive portal as the user cannot
1405 // sign-in to an empty page. Probably the result of a broken transparent proxy.
1406 // See http://b/9972012.
1407 validationLog(probeType, url,
1408 "200 response with Content-length=0 interpreted as 204 response.");
1409 httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1410 } else if (urlConnection.getContentLengthLong() == -1) {
1411 // When no Content-length (default value == -1), attempt to read a byte from the
1412 // response. Do not use available() as it is unreliable. See http://b/33498325.
1413 if (urlConnection.getInputStream().read() == -1) {
1414 validationLog(
1415 probeType, url, "Empty 200 response interpreted as 204 response.");
1416 httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
1417 }
1418 }
1419 }
1420 } catch (IOException e) {
1421 validationLog(probeType, url, "Probe failed with exception " + e);
1422 if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
1423 // TODO: Ping gateway and DNS server and log results.
1424 }
1425 } finally {
1426 if (urlConnection != null) {
1427 urlConnection.disconnect();
1428 }
1429 TrafficStats.setThreadStatsTag(oldTag);
1430 }
1431 logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
1432
1433 if (probeSpec == null) {
1434 return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString());
1435 } else {
1436 return probeSpec.getResult(httpResponseCode, redirectUrl);
1437 }
1438 }
1439
1440 private CaptivePortalProbeResult sendParallelHttpProbes(
1441 ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
1442 // Number of probes to wait for. If a probe completes with a conclusive answer
1443 // it shortcuts the latch immediately by forcing the count to 0.
1444 final CountDownLatch latch = new CountDownLatch(2);
1445
1446 final class ProbeThread extends Thread {
1447 private final boolean mIsHttps;
1448 private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;
1449
1450 ProbeThread(boolean isHttps) {
1451 mIsHttps = isHttps;
1452 }
1453
1454 public CaptivePortalProbeResult result() {
1455 return mResult;
1456 }
1457
1458 @Override
1459 public void run() {
1460 if (mIsHttps) {
1461 mResult =
1462 sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
1463 } else {
1464 mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP);
1465 }
1466 if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
1467 // Stop waiting immediately if https succeeds or if http finds a portal.
1468 while (latch.getCount() > 0) {
1469 latch.countDown();
1470 }
1471 }
1472 // Signal this probe has completed.
1473 latch.countDown();
1474 }
1475 }
1476
1477 final ProbeThread httpsProbe = new ProbeThread(true);
1478 final ProbeThread httpProbe = new ProbeThread(false);
1479
1480 try {
1481 httpsProbe.start();
1482 httpProbe.start();
1483 latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
1484 } catch (InterruptedException e) {
1485 validationLog("Error: probes wait interrupted!");
1486 return CaptivePortalProbeResult.FAILED;
1487 }
1488
1489 final CaptivePortalProbeResult httpsResult = httpsProbe.result();
1490 final CaptivePortalProbeResult httpResult = httpProbe.result();
1491
1492 // Look for a conclusive probe result first.
1493 if (httpResult.isPortal()) {
1494 return httpResult;
1495 }
1496 // httpsResult.isPortal() is not expected, but check it nonetheless.
1497 if (httpsResult.isPortal() || httpsResult.isSuccessful()) {
1498 return httpsResult;
1499 }
1500 // If a fallback method exists, use it to retry portal detection.
1501 // If we have new-style probe specs, use those. Otherwise, use the fallback URLs.
1502 final CaptivePortalProbeSpec probeSpec = nextFallbackSpec();
1503 final URL fallbackUrl = (probeSpec != null) ? probeSpec.getUrl() : nextFallbackUrl();
1504 if (fallbackUrl != null) {
1505 CaptivePortalProbeResult result = sendHttpProbe(fallbackUrl, PROBE_FALLBACK, probeSpec);
1506 if (result.isPortal()) {
1507 return result;
1508 }
1509 }
1510 // Otherwise wait until http and https probes completes and use their results.
1511 try {
1512 httpProbe.join();
1513 if (httpProbe.result().isPortal()) {
1514 return httpProbe.result();
1515 }
1516 httpsProbe.join();
1517 return httpsProbe.result();
1518 } catch (InterruptedException e) {
1519 validationLog("Error: http or https probe wait interrupted!");
1520 return CaptivePortalProbeResult.FAILED;
1521 }
1522 }
1523
1524 private URL makeURL(String url) {
1525 if (url != null) {
1526 try {
1527 return new URL(url);
1528 } catch (MalformedURLException e) {
1529 validationLog("Bad URL: " + url);
1530 }
1531 }
1532 return null;
1533 }
1534
1535 /**
1536 * @param responseReceived - whether or not we received a valid HTTP response to our request.
1537 * If false, isCaptivePortal and responseTimestampMs are ignored
1538 * TODO: This should be moved to the transports. The latency could be passed to the transports
1539 * along with the captive portal result. Currently the TYPE_MOBILE broadcasts appear unused so
1540 * perhaps this could just be added to the WiFi transport only.
1541 */
1542 private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
1543 long requestTimestampMs, long responseTimestampMs) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001544 if (!mSystemReady) {
1545 return;
1546 }
1547
1548 Intent latencyBroadcast =
1549 new Intent(NetworkMonitorUtils.ACTION_NETWORK_CONDITIONS_MEASURED);
1550 if (mNetworkCapabilities.hasTransport(TRANSPORT_WIFI)) {
Chiachang Wangb04f81c2019-02-14 09:30:58 +08001551 if (!mWifiManager.isScanAlwaysAvailable()) {
1552 return;
1553 }
1554
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001555 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
1556 if (currentWifiInfo != null) {
1557 // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
1558 // surrounded by double quotation marks (thus violating the Javadoc), but this
1559 // was changed to match the Javadoc in API 17. Since clients may have started
1560 // sanitizing the output of this method since API 17 was released, we should
1561 // not change it here as it would become impossible to tell whether the SSID is
1562 // simply being surrounded by quotes due to the API, or whether those quotes
1563 // are actually part of the SSID.
1564 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_SSID,
1565 currentWifiInfo.getSSID());
1566 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_BSSID,
1567 currentWifiInfo.getBSSID());
1568 } else {
1569 if (VDBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
1570 return;
1571 }
1572 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CONNECTIVITY_TYPE, TYPE_WIFI);
1573 } else if (mNetworkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
Chiachang Wangb04f81c2019-02-14 09:30:58 +08001574 // TODO(b/123893112): Support multi-sim.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001575 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_NETWORK_TYPE,
1576 mTelephonyManager.getNetworkType());
Chiachang Wangb04f81c2019-02-14 09:30:58 +08001577 final ServiceState dataSs = mTelephonyManager.getServiceState();
1578 if (dataSs == null) {
1579 logw("failed to retrieve ServiceState");
1580 return;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001581 }
Chiachang Wangb04f81c2019-02-14 09:30:58 +08001582 // See if the data sub is registered for PS services on cell.
1583 final NetworkRegistrationState nrs = dataSs.getNetworkRegistrationState(
1584 NetworkRegistrationState.DOMAIN_PS,
1585 AccessNetworkConstants.TransportType.WWAN);
1586 latencyBroadcast.putExtra(
1587 NetworkMonitorUtils.EXTRA_CELL_ID,
1588 nrs == null ? null : nrs.getCellIdentity());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001589 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CONNECTIVITY_TYPE, TYPE_MOBILE);
1590 } else {
1591 return;
1592 }
1593 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_RESPONSE_RECEIVED,
1594 responseReceived);
1595 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_REQUEST_TIMESTAMP_MS,
1596 requestTimestampMs);
1597
1598 if (responseReceived) {
1599 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_IS_CAPTIVE_PORTAL,
1600 isCaptivePortal);
1601 latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_RESPONSE_TIMESTAMP_MS,
1602 responseTimestampMs);
1603 }
1604 mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT,
1605 NetworkMonitorUtils.PERMISSION_ACCESS_NETWORK_CONDITIONS);
1606 }
1607
1608 private void logNetworkEvent(int evtype) {
1609 int[] transports = mNetworkCapabilities.getTransportTypes();
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +09001610 mMetricsLog.log(mNetwork, transports, new NetworkEvent(evtype));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001611 }
1612
1613 private int networkEventType(ValidationStage s, EvaluationResult r) {
1614 if (s.mIsFirstValidation) {
1615 if (r.mIsValidated) {
1616 return NetworkEvent.NETWORK_FIRST_VALIDATION_SUCCESS;
1617 } else {
1618 return NetworkEvent.NETWORK_FIRST_VALIDATION_PORTAL_FOUND;
1619 }
1620 } else {
1621 if (r.mIsValidated) {
1622 return NetworkEvent.NETWORK_REVALIDATION_SUCCESS;
1623 } else {
1624 return NetworkEvent.NETWORK_REVALIDATION_PORTAL_FOUND;
1625 }
1626 }
1627 }
1628
1629 private void maybeLogEvaluationResult(int evtype) {
1630 if (mEvaluationTimer.isRunning()) {
1631 int[] transports = mNetworkCapabilities.getTransportTypes();
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +09001632 mMetricsLog.log(mNetwork, transports,
1633 new NetworkEvent(evtype, mEvaluationTimer.stop()));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001634 mEvaluationTimer.reset();
1635 }
1636 }
1637
1638 private void logValidationProbe(long durationMs, int probeType, int probeResult) {
1639 int[] transports = mNetworkCapabilities.getTransportTypes();
1640 boolean isFirstValidation = validationStage().mIsFirstValidation;
Remi NGUYEN VANc148d8b2019-01-19 21:13:24 +09001641 ValidationProbeEvent ev = new ValidationProbeEvent.Builder()
1642 .setProbeType(probeType, isFirstValidation)
1643 .setReturnCode(probeResult)
1644 .setDurationMs(durationMs)
1645 .build();
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +09001646 mMetricsLog.log(mNetwork, transports, ev);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001647 }
1648
1649 @VisibleForTesting
1650 static class Dependencies {
1651 public Network getPrivateDnsBypassNetwork(Network network) {
1652 return new OneAddressPerFamilyNetwork(network);
1653 }
1654
1655 public Random getRandom() {
1656 return new Random();
1657 }
1658
1659 /**
1660 * Get the captive portal server HTTP URL that is configured on the device.
1661 */
1662 public String getCaptivePortalServerHttpUrl(Context context) {
1663 return NetworkMonitorUtils.getCaptivePortalServerHttpUrl(context);
1664 }
1665
1666 /**
1667 * Get the value of a global integer setting.
1668 * @param symbol Name of the setting
1669 * @param defaultValue Value to return if the setting is not defined.
1670 */
1671 public int getSetting(Context context, String symbol, int defaultValue) {
1672 return Settings.Global.getInt(context.getContentResolver(), symbol, defaultValue);
1673 }
1674
1675 /**
1676 * Get the value of a global String setting.
1677 * @param symbol Name of the setting
1678 * @param defaultValue Value to return if the setting is not defined.
1679 */
1680 public String getSetting(Context context, String symbol, String defaultValue) {
1681 final String value = Settings.Global.getString(context.getContentResolver(), symbol);
1682 return value != null ? value : defaultValue;
1683 }
1684
1685 public static final Dependencies DEFAULT = new Dependencies();
1686 }
1687
1688 /**
1689 * Methods in this class perform no locking because all accesses are performed on the state
1690 * machine's thread. Need to consider the thread safety if it ever could be accessed outside the
1691 * state machine.
1692 */
1693 @VisibleForTesting
1694 protected class DnsStallDetector {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001695 private int mConsecutiveTimeoutCount = 0;
1696 private int mSize;
1697 final DnsResult[] mDnsEvents;
1698 final RingBufferIndices mResultIndices;
1699
1700 DnsStallDetector(int size) {
1701 mSize = Math.max(DEFAULT_DNS_LOG_SIZE, size);
1702 mDnsEvents = new DnsResult[mSize];
1703 mResultIndices = new RingBufferIndices(mSize);
1704 }
1705
1706 @VisibleForTesting
1707 protected void accumulateConsecutiveDnsTimeoutCount(int code) {
1708 final DnsResult result = new DnsResult(code);
1709 mDnsEvents[mResultIndices.add()] = result;
1710 if (result.isTimeout()) {
1711 mConsecutiveTimeoutCount++;
1712 } else {
1713 // Keep the event in mDnsEvents without clearing it so that there are logs to do the
1714 // simulation and analysis.
1715 mConsecutiveTimeoutCount = 0;
1716 }
1717 }
1718
1719 private boolean isDataStallSuspected(int timeoutCountThreshold, int validTime) {
1720 if (timeoutCountThreshold <= 0) {
1721 Log.wtf(TAG, "Timeout count threshold should be larger than 0.");
1722 return false;
1723 }
1724
1725 // Check if the consecutive timeout count reach the threshold or not.
1726 if (mConsecutiveTimeoutCount < timeoutCountThreshold) {
1727 return false;
1728 }
1729
1730 // Check if the target dns event index is valid or not.
1731 final int firstConsecutiveTimeoutIndex =
1732 mResultIndices.indexOf(mResultIndices.size() - timeoutCountThreshold);
1733
1734 // If the dns timeout events happened long time ago, the events are meaningless for
1735 // data stall evaluation. Thus, check if the first consecutive timeout dns event
1736 // considered in the evaluation happened in defined threshold time.
1737 final long now = SystemClock.elapsedRealtime();
1738 final long firstTimeoutTime = now - mDnsEvents[firstConsecutiveTimeoutIndex].mTimeStamp;
1739 return (firstTimeoutTime < validTime);
1740 }
1741
1742 int getConsecutiveTimeoutCount() {
1743 return mConsecutiveTimeoutCount;
1744 }
1745 }
1746
1747 private static class DnsResult {
1748 // TODO: Need to move the DNS return code definition to a specific class once unify DNS
1749 // response code is done.
1750 private static final int RETURN_CODE_DNS_TIMEOUT = 255;
1751
1752 private final long mTimeStamp;
1753 private final int mReturnCode;
1754
1755 DnsResult(int code) {
1756 mTimeStamp = SystemClock.elapsedRealtime();
1757 mReturnCode = code;
1758 }
1759
1760 private boolean isTimeout() {
1761 return mReturnCode == RETURN_CODE_DNS_TIMEOUT;
1762 }
1763 }
1764
1765
1766 @VisibleForTesting
1767 protected DnsStallDetector getDnsStallDetector() {
1768 return mDnsStallDetector;
1769 }
1770
1771 private boolean dataStallEvaluateTypeEnabled(int type) {
1772 return (mDataStallEvaluationType & (1 << type)) != 0;
1773 }
1774
1775 @VisibleForTesting
1776 protected long getLastProbeTime() {
1777 return mLastProbeTime;
1778 }
1779
1780 @VisibleForTesting
1781 protected boolean isDataStall() {
1782 boolean result = false;
1783 // Reevaluation will generate traffic. Thus, set a minimal reevaluation timer to limit the
1784 // possible traffic cost in metered network.
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +09001785 if (!mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001786 && (SystemClock.elapsedRealtime() - getLastProbeTime()
1787 < mDataStallMinEvaluateTime)) {
1788 return false;
1789 }
1790
1791 // Check dns signal. Suspect it may be a data stall if both :
1792 // 1. The number of consecutive DNS query timeouts > mConsecutiveDnsTimeoutThreshold.
1793 // 2. Those consecutive DNS queries happened in the last mValidDataStallDnsTimeThreshold ms.
1794 if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_DNS)) {
1795 if (mDnsStallDetector.isDataStallSuspected(mConsecutiveDnsTimeoutThreshold,
1796 mDataStallValidDnsTimeThreshold)) {
1797 result = true;
1798 logNetworkEvent(NetworkEvent.NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND);
1799 }
1800 }
1801
1802 if (VDBG_STALL) {
1803 log("isDataStall: result=" + result + ", consecutive dns timeout count="
1804 + mDnsStallDetector.getConsecutiveTimeoutCount());
1805 }
1806
1807 return result;
1808 }
1809}