blob: fe393f9a8db8eaa07b7b8df6a76a6d9e1d1eb9b4 [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;
Chiachang Wang1c67f4e2019-05-09 21:28:47 +080024import static android.net.DnsResolver.FLAG_EMPTY;
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +090025import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
26import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY;
27import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID;
Chiachang Wang813ee472019-05-23 16:29:30 +080028import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
29import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK;
30import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP;
31import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
32import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
33import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
34import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +090035import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
Niklas Lindgren0c904882018-12-07 11:08:04 +010036import static android.net.captiveportal.CaptivePortalProbeSpec.parseCaptivePortalProbeSpecs;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090037import static android.net.metrics.ValidationProbeEvent.DNS_FAILURE;
38import static android.net.metrics.ValidationProbeEvent.DNS_SUCCESS;
39import static android.net.metrics.ValidationProbeEvent.PROBE_FALLBACK;
40import static android.net.metrics.ValidationProbeEvent.PROBE_PRIVDNS;
Chiachang Wang9a87f802019-04-08 19:06:21 +080041import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD;
42import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_EVALUATION_TYPE;
43import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_MIN_EVALUATE_INTERVAL;
Chiachang Wanga5716bf2019-11-20 16:13:07 +080044import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_TCP_POLLING_INTERVAL;
Chiachang Wang9a87f802019-04-08 19:06:21 +080045import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_VALID_DNS_TIME_THRESHOLD;
46import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_DNS;
Chiachang Wang0b34ae62020-05-21 07:03:23 +000047import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_NONE;
Chiachang Wanga5716bf2019-11-20 16:13:07 +080048import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_TCP;
Chiachang Wang9a87f802019-04-08 19:06:21 +080049import static android.net.util.DataStallUtils.DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD;
50import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_EVALUATION_TYPES;
51import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS;
52import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS;
53import static android.net.util.DataStallUtils.DEFAULT_DNS_LOG_SIZE;
Chiachang Wanga5716bf2019-11-20 16:13:07 +080054import static android.net.util.DataStallUtils.DEFAULT_TCP_POLLING_INTERVAL_MS;
Chiachang Wang79a6da32019-04-17 17:00:54 +080055import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
Chiachang Wang969eb752019-04-25 09:47:27 +080056import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_URL;
57import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_HTTPS_URL;
58import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_HTTP_URL;
59import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_MODE;
60import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_MODE_IGNORE;
61import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_MODE_PROMPT;
Chiachang Wang79a6da32019-04-17 17:00:54 +080062import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
Chiachang Wanga64bb702020-03-31 07:50:59 +000063import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_HTTPS_URLS;
64import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_HTTP_URLS;
Chiachang Wang79a6da32019-04-17 17:00:54 +080065import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USER_AGENT;
66import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
Chiachang Wang44ee27b2020-04-10 09:02:45 +080067import static android.net.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT;
68import static android.net.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
Chiachang Wanga64bb702020-03-31 07:50:59 +000069import static android.net.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_HTTPS_URLS;
70import static android.net.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_HTTP_URLS;
Automerger Merge Workerfaac06e2020-03-09 09:23:38 +000071import static android.net.util.NetworkStackUtils.DISMISS_PORTAL_IN_VALIDATED_NETWORK;
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +000072import static android.net.util.NetworkStackUtils.DNS_PROBE_PRIVATE_IP_NO_INTERNET_VERSION;
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +000073import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
74import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
75import static android.net.util.NetworkStackUtils.TEST_URL_EXPIRATION_TIME;
Remi NGUYEN VANabeaaf72019-01-20 13:48:19 +090076import static android.net.util.NetworkStackUtils.isEmpty;
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +000077import static android.net.util.NetworkStackUtils.isIPv6ULA;
Chiachang Wangbc1a1012019-09-06 15:05:32 +080078import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090079
Cody Kesting782cbfa2020-01-06 15:51:59 -080080import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_DNS_EVENTS;
81import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_TCP_METRICS;
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +000082import static com.android.networkstack.apishim.ConstantsShim.TRANSPORT_TEST;
Chiachang Wangeb619222019-07-03 20:52:08 +080083import static com.android.networkstack.util.DnsUtils.PRIVATE_DNS_PROBE_HOST_SUFFIX;
Chiachang Wang1c67f4e2019-05-09 21:28:47 +080084import static com.android.networkstack.util.DnsUtils.TYPE_ADDRCONFIG;
85
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090086import android.app.PendingIntent;
87import android.content.BroadcastReceiver;
88import android.content.Context;
89import android.content.Intent;
90import android.content.IntentFilter;
lucaslin9b4dfab2019-12-17 23:06:12 +080091import android.content.pm.PackageManager;
92import android.content.res.Configuration;
Niklas Lindgren0c904882018-12-07 11:08:04 +010093import android.content.res.Resources;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090094import android.net.ConnectivityManager;
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +000095import android.net.DataStallReportParcelable;
Lorenzo Colitti171cfd22019-04-18 13:44:32 +090096import android.net.DnsResolver;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +090097import android.net.INetworkMonitorCallbacks;
98import android.net.LinkProperties;
99import android.net.Network;
100import android.net.NetworkCapabilities;
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000101import android.net.NetworkTestResultParcelable;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900102import android.net.ProxyInfo;
103import android.net.TrafficStats;
104import android.net.Uri;
Chiachang Wang50865812020-04-14 16:26:24 +0000105import android.net.captiveportal.CapportApiProbeResult;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900106import android.net.captiveportal.CaptivePortalProbeResult;
107import android.net.captiveportal.CaptivePortalProbeSpec;
108import android.net.metrics.IpConnectivityLog;
109import android.net.metrics.NetworkEvent;
110import android.net.metrics.ValidationProbeEvent;
111import android.net.shared.NetworkMonitorUtils;
112import android.net.shared.PrivateDnsConfig;
Chiachang Wang0b34ae62020-05-21 07:03:23 +0000113import android.net.util.DataStallUtils.EvaluationType;
Chiachang Wang9a87f802019-04-08 19:06:21 +0800114import android.net.util.NetworkStackUtils;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900115import android.net.util.SharedLog;
116import android.net.util.Stopwatch;
117import android.net.wifi.WifiInfo;
118import android.net.wifi.WifiManager;
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +0900119import android.os.Build;
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +0900120import android.os.Bundle;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900121import android.os.Message;
lucaslin9b4dfab2019-12-17 23:06:12 +0800122import android.os.Process;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900123import android.os.RemoteException;
124import android.os.SystemClock;
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +0000125import android.provider.DeviceConfig;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900126import android.provider.Settings;
Frank Lie04f8b72020-06-22 12:45:27 +0000127import android.stats.connectivity.ProbeResult;
128import android.stats.connectivity.ProbeType;
lucaslin9b4dfab2019-12-17 23:06:12 +0800129import android.telephony.CellIdentityNr;
130import android.telephony.CellInfo;
131import android.telephony.CellInfoGsm;
132import android.telephony.CellInfoLte;
133import android.telephony.CellInfoNr;
134import android.telephony.CellInfoTdscdma;
135import android.telephony.CellInfoWcdma;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800136import android.telephony.CellSignalStrength;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +0800137import android.telephony.SignalStrength;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900138import android.telephony.TelephonyManager;
139import android.text.TextUtils;
140import android.util.Log;
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900141import android.util.Pair;
Lucas Lin044be5f2020-04-16 02:17:00 +0000142import android.util.SparseArray;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900143
Niklas Lindgren0c904882018-12-07 11:08:04 +0100144import androidx.annotation.ArrayRes;
lucaslin9b4dfab2019-12-17 23:06:12 +0800145import androidx.annotation.BoolRes;
lucaslinc3d9f712020-04-10 03:28:12 +0000146import androidx.annotation.IntegerRes;
Remi NGUYEN VAN87790c22019-09-12 14:39:24 +0900147import androidx.annotation.NonNull;
148import androidx.annotation.Nullable;
Niklas Lindgren0c904882018-12-07 11:08:04 +0100149import androidx.annotation.StringRes;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900150import androidx.annotation.VisibleForTesting;
Niklas Lindgren0c904882018-12-07 11:08:04 +0100151
Frank Lie04f8b72020-06-22 12:45:27 +0000152import com.android.internal.annotations.GuardedBy;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900153import com.android.internal.util.RingBufferIndices;
154import com.android.internal.util.State;
155import com.android.internal.util.StateMachine;
Chalard Jeanac5b8992019-04-09 11:16:56 +0900156import com.android.internal.util.TrafficStatsConstants;
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900157import com.android.networkstack.NetworkStackNotifier;
Remi NGUYEN VANb4f48692019-03-20 14:22:49 +0900158import com.android.networkstack.R;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900159import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
160import com.android.networkstack.apishim.NetworkInformationShimImpl;
Remi NGUYEN VAN473501f2020-05-15 08:20:50 +0000161import com.android.networkstack.apishim.common.CaptivePortalDataShim;
162import com.android.networkstack.apishim.common.ShimUtils;
163import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
Chiachang Wang80242272019-04-11 21:24:28 +0800164import com.android.networkstack.metrics.DataStallDetectionStats;
165import com.android.networkstack.metrics.DataStallStatsUtils;
Frank Lie04f8b72020-06-22 12:45:27 +0000166import com.android.networkstack.metrics.NetworkValidationMetrics;
Chiachang Wanga5716bf2019-11-20 16:13:07 +0800167import com.android.networkstack.netlink.TcpSocketTracker;
Chiachang Wang1c67f4e2019-05-09 21:28:47 +0800168import com.android.networkstack.util.DnsUtils;
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900169import com.android.server.NetworkStackService.NetworkStackServiceManager;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900170
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900171import org.json.JSONException;
172import org.json.JSONObject;
173
lucaslinc3d9f712020-04-10 03:28:12 +0000174import java.io.BufferedInputStream;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900175import java.io.IOException;
Remi NGUYEN VAN2d909a72019-12-24 18:15:52 +0900176import java.io.InputStream;
177import java.io.InputStreamReader;
Lucas Linfc5814c2020-05-05 08:15:19 +0000178import java.io.InterruptedIOException;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900179import java.net.HttpURLConnection;
180import java.net.InetAddress;
181import java.net.MalformedURLException;
182import java.net.URL;
183import java.net.UnknownHostException;
Remi NGUYEN VAN2d909a72019-12-24 18:15:52 +0900184import java.nio.charset.Charset;
185import java.nio.charset.StandardCharsets;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900186import java.util.ArrayList;
187import java.util.Arrays;
188import java.util.Collections;
lucaslin9b4dfab2019-12-17 23:06:12 +0800189import java.util.HashMap;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900190import java.util.LinkedHashMap;
191import java.util.List;
lucaslin9b4dfab2019-12-17 23:06:12 +0800192import java.util.Map;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900193import java.util.Objects;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900194import java.util.Random;
Chiachang Wange797c9b2019-11-28 14:18:47 +0800195import java.util.StringJoiner;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900196import java.util.UUID;
Chiachang Wang3278b382020-04-16 13:04:02 +0000197import java.util.concurrent.CompletionService;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900198import java.util.concurrent.CountDownLatch;
Chiachang Wang3278b382020-04-16 13:04:02 +0000199import java.util.concurrent.ExecutionException;
200import java.util.concurrent.ExecutorCompletionService;
201import java.util.concurrent.ExecutorService;
202import java.util.concurrent.Executors;
203import java.util.concurrent.Future;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900204import java.util.concurrent.TimeUnit;
Lucas Linfc5814c2020-05-05 08:15:19 +0000205import java.util.concurrent.atomic.AtomicInteger;
Niklas Lindgren0c904882018-12-07 11:08:04 +0100206import java.util.function.Function;
Remi NGUYEN VAN2d909a72019-12-24 18:15:52 +0900207import java.util.regex.Matcher;
208import java.util.regex.Pattern;
lucaslinc3d9f712020-04-10 03:28:12 +0000209import java.util.regex.PatternSyntaxException;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900210
211/**
212 * {@hide}
213 */
214public class NetworkMonitor extends StateMachine {
215 private static final String TAG = NetworkMonitor.class.getSimpleName();
216 private static final boolean DBG = true;
217 private static final boolean VDBG = false;
218 private static final boolean VDBG_STALL = Log.isLoggable(TAG, Log.DEBUG);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900219 private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) "
220 + "AppleWebKit/537.36 (KHTML, like Gecko) "
221 + "Chrome/60.0.3112.32 Safari/537.36";
222
Lorenzo Colitti171cfd22019-04-18 13:44:32 +0900223 @VisibleForTesting
224 static final String CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT =
225 "captive_portal_dns_probe_timeout";
226
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900227 private static final int SOCKET_TIMEOUT_MS = 10000;
228 private static final int PROBE_TIMEOUT_MS = 3000;
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +0000229 private static final long TEST_URL_EXPIRATION_MS = TimeUnit.MINUTES.toMillis(10);
Lorenzo Colitti171cfd22019-04-18 13:44:32 +0900230
Lucas Lin044be5f2020-04-16 02:17:00 +0000231 private static final int UNSET_MCC_OR_MNC = -1;
232
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900233 private static final int CAPPORT_API_MAX_JSON_LENGTH = 4096;
234 private static final String ACCEPT_HEADER = "Accept";
235 private static final String CONTENT_TYPE_HEADER = "Content-Type";
236 private static final String CAPPORT_API_CONTENT_TYPE = "application/captive+json";
237
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900238 enum EvaluationResult {
239 VALIDATED(true),
240 CAPTIVE_PORTAL(false);
241 final boolean mIsValidated;
242 EvaluationResult(boolean isValidated) {
243 this.mIsValidated = isValidated;
244 }
245 }
246
247 enum ValidationStage {
248 FIRST_VALIDATION(true),
249 REVALIDATION(false);
250 final boolean mIsFirstValidation;
251 ValidationStage(boolean isFirstValidation) {
252 this.mIsFirstValidation = isFirstValidation;
253 }
254 }
255
Lucas Lin044be5f2020-04-16 02:17:00 +0000256 @VisibleForTesting
257 protected static final class MccMncOverrideInfo {
258 public final int mcc;
259 public final int mnc;
260 MccMncOverrideInfo(int mcc, int mnc) {
261 this.mcc = mcc;
262 this.mnc = mnc;
263 }
264 }
265
266 @VisibleForTesting
267 protected static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
268
269 static {
270 // CTC
271 sCarrierIdToMccMnc.put(1854, new MccMncOverrideInfo(460, 03));
272 }
273
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900274 /**
275 * ConnectivityService has sent a notification to indicate that network has connected.
276 * Initiates Network Validation.
277 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900278 private static final int CMD_NETWORK_CONNECTED = 1;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900279
280 /**
281 * Message to self indicating it's time to evaluate a network's connectivity.
282 * arg1 = Token to ignore old messages.
283 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900284 private static final int CMD_REEVALUATE = 6;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900285
286 /**
287 * ConnectivityService has sent a notification to indicate that network has disconnected.
288 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900289 private static final int CMD_NETWORK_DISCONNECTED = 7;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900290
291 /**
292 * Force evaluation even if it has succeeded in the past.
293 * arg1 = UID responsible for requesting this reeval. Will be billed for data.
294 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900295 private static final int CMD_FORCE_REEVALUATION = 8;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900296
297 /**
298 * Message to self indicating captive portal app finished.
299 * arg1 = one of: APP_RETURN_DISMISSED,
300 * APP_RETURN_UNWANTED,
301 * APP_RETURN_WANTED_AS_IS
302 * obj = mCaptivePortalLoggedInResponseToken as String
303 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900304 private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = 9;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900305
306 /**
307 * Message indicating sign-in app should be launched.
308 * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the
309 * user touches the sign in notification, or sent by
310 * ConnectivityService when the user touches the "sign into
311 * network" button in the wifi access point detail page.
312 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900313 private static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = 11;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900314
315 /**
316 * Retest network to see if captive portal is still in place.
317 * arg1 = UID responsible for requesting this reeval. Will be billed for data.
318 * 0 indicates self-initiated, so nobody to blame.
319 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900320 private static final int CMD_CAPTIVE_PORTAL_RECHECK = 12;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900321
322 /**
323 * ConnectivityService notifies NetworkMonitor of settings changes to
324 * Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
325 * strict mode, then an event is sent back to ConnectivityService with the
326 * result of the resolution attempt.
327 *
328 * A separate message is used to trigger (re)evaluation of the Private DNS
329 * configuration, so that the message can be handled as needed in different
330 * states, including being ignored until after an ongoing captive portal
331 * validation phase is completed.
332 */
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900333 private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = 13;
334 private static final int CMD_EVALUATE_PRIVATE_DNS = 15;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900335
336 /**
337 * Message to self indicating captive portal detection is completed.
338 * obj = CaptivePortalProbeResult for detection result;
339 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900340 private static final int CMD_PROBE_COMPLETE = 16;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900341
342 /**
343 * ConnectivityService notifies NetworkMonitor of DNS query responses event.
344 * arg1 = returncode in OnDnsEvent which indicates the response code for the DNS query.
345 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900346 private static final int EVENT_DNS_NOTIFICATION = 17;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900347
lucaslinb0573962019-03-12 13:08:03 +0800348 /**
349 * ConnectivityService notifies NetworkMonitor that the user accepts partial connectivity and
350 * NetworkMonitor should ignore the https probe.
351 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900352 private static final int EVENT_ACCEPT_PARTIAL_CONNECTIVITY = 18;
353
354 /**
355 * ConnectivityService notifies NetworkMonitor of changed LinkProperties.
356 * obj = new LinkProperties.
357 */
358 private static final int EVENT_LINK_PROPERTIES_CHANGED = 19;
359
360 /**
361 * ConnectivityService notifies NetworkMonitor of changed NetworkCapabilities.
362 * obj = new NetworkCapabilities.
363 */
364 private static final int EVENT_NETWORK_CAPABILITIES_CHANGED = 20;
lucaslinb0573962019-03-12 13:08:03 +0800365
Chiachang Wanga5716bf2019-11-20 16:13:07 +0800366 /**
367 * Message to self to poll current tcp status from kernel.
368 */
369 private static final int EVENT_POLL_TCPINFO = 21;
Lucas Linfc5814c2020-05-05 08:15:19 +0000370
371 /**
372 * Message to self to do the bandwidth check in EvaluatingBandwidthState.
373 */
374 private static final int CMD_EVALUATE_BANDWIDTH = 22;
375
376 /**
377 * Message to self to know the bandwidth check is completed.
378 */
379 private static final int CMD_BANDWIDTH_CHECK_COMPLETE = 23;
380
381 /**
382 * Message to self to know the bandwidth check is timeouted.
383 */
384 private static final int CMD_BANDWIDTH_CHECK_TIMEOUT = 24;
385
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900386 // Start mReevaluateDelayMs at this value and double.
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +0000387 @VisibleForTesting
388 static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900389 private static final int MAX_REEVALUATE_DELAY_MS = 10 * 60 * 1000;
Lucas Linfc5814c2020-05-05 08:15:19 +0000390 // Default timeout of evaluating network bandwidth.
391 private static final int DEFAULT_EVALUATING_BANDWIDTH_TIMEOUT_MS = 10_000;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900392 // Before network has been evaluated this many times, ignore repeated reevaluate requests.
393 private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
394 private int mReevaluateToken = 0;
395 private static final int NO_UID = 0;
396 private static final int INVALID_UID = -1;
397 private int mUidResponsibleForReeval = INVALID_UID;
398 // Stop blaming UID that requested re-evaluation after this many attempts.
399 private static final int BLAME_FOR_EVALUATION_ATTEMPTS = 5;
400 // Delay between reevaluations once a captive portal has been found.
401 private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10 * 60 * 1000;
Chiachang Wang813ee472019-05-23 16:29:30 +0800402 private static final int NETWORK_VALIDATION_RESULT_INVALID = 0;
Chiachang Wang3278b382020-04-16 13:04:02 +0000403 // Max thread pool size for parallel probing. Fixed thread pool size to control the thread
404 // number used for either HTTP or HTTPS probing.
405 @VisibleForTesting
406 static final int MAX_PROBE_THREAD_POOL_SIZE = 5;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900407 private String mPrivateDnsProviderHostname = "";
408
409 private final Context mContext;
410 private final INetworkMonitorCallbacks mCallback;
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +0900411 private final int mCallbackVersion;
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +0900412 private final Network mCleartextDnsNetwork;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900413 private final Network mNetwork;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900414 private final TelephonyManager mTelephonyManager;
415 private final WifiManager mWifiManager;
416 private final ConnectivityManager mCm;
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900417 @Nullable
418 private final NetworkStackNotifier mNotifier;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900419 private final IpConnectivityLog mMetricsLog;
420 private final Dependencies mDependencies;
Chiachang Wanga5716bf2019-11-20 16:13:07 +0800421 private final TcpSocketTracker mTcpTracker;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900422 // Configuration values for captive portal detection probes.
423 private final String mCaptivePortalUserAgent;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900424 private final URL[] mCaptivePortalFallbackUrls;
Chiachang Wanga64bb702020-03-31 07:50:59 +0000425 @NonNull
426 private final URL[] mCaptivePortalHttpUrls;
427 @NonNull
428 private final URL[] mCaptivePortalHttpsUrls;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900429 @Nullable
430 private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs;
Lucas Linfc5814c2020-05-05 08:15:19 +0000431 // Configuration values for network bandwidth check.
432 @Nullable
433 private final String mEvaluatingBandwidthUrl;
434 private final int mMaxRetryTimerMs;
435 private final int mEvaluatingBandwidthTimeoutMs;
436 private final AtomicInteger mNextEvaluatingBandwidthThreadId = new AtomicInteger(1);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900437
438 private NetworkCapabilities mNetworkCapabilities;
439 private LinkProperties mLinkProperties;
440
441 @VisibleForTesting
442 protected boolean mIsCaptivePortalCheckEnabled;
443
444 private boolean mUseHttps;
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900445 /**
446 * The total number of completed validation attempts (network validated or a captive portal was
447 * detected) for this NetworkMonitor instance.
448 * This does not include attempts that were interrupted, retried or finished with a result that
449 * is not success or portal. See {@code mValidationIndex} in {@link NetworkValidationMetrics}
450 * for a count of all attempts.
451 * TODO: remove when removing legacy metrics.
452 */
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900453 private int mValidations = 0;
454
455 // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
456 private boolean mUserDoesNotWant = false;
457 // Avoids surfacing "Sign in to network" notification.
458 private boolean mDontDisplaySigninNotification = false;
Lucas Linfc5814c2020-05-05 08:15:19 +0000459 // Set to true once the evaluating network bandwidth is passed or the captive portal respond
460 // APP_RETURN_WANTED_AS_IS which means the user wants to use this network anyway.
461 @VisibleForTesting
462 protected boolean mIsBandwidthCheckPassedOrIgnored = false;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900463
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900464 private final State mDefaultState = new DefaultState();
465 private final State mValidatedState = new ValidatedState();
466 private final State mMaybeNotifyState = new MaybeNotifyState();
467 private final State mEvaluatingState = new EvaluatingState();
468 private final State mCaptivePortalState = new CaptivePortalState();
469 private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
470 private final State mProbingState = new ProbingState();
471 private final State mWaitingForNextProbeState = new WaitingForNextProbeState();
Lucas Linfc5814c2020-05-05 08:15:19 +0000472 private final State mEvaluatingBandwidthState = new EvaluatingBandwidthState();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900473
474 private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
475
476 private final SharedLog mValidationLogs;
477
478 private final Stopwatch mEvaluationTimer = new Stopwatch();
479
480 // This variable is set before transitioning to the mCaptivePortalState.
Chiachang Wang50865812020-04-14 16:26:24 +0000481 private CaptivePortalProbeResult mLastPortalProbeResult =
482 CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900483
484 // Random generator to select fallback URL index
485 private final Random mRandom;
486 private int mNextFallbackUrlIndex = 0;
487
488
489 private int mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
490 private int mEvaluateAttempts = 0;
491 private volatile int mProbeToken = 0;
492 private final int mConsecutiveDnsTimeoutThreshold;
493 private final int mDataStallMinEvaluateTime;
494 private final int mDataStallValidDnsTimeThreshold;
495 private final int mDataStallEvaluationType;
Chiachang Wang8e232042020-01-17 18:01:59 +0800496 @Nullable
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900497 private final DnsStallDetector mDnsStallDetector;
498 private long mLastProbeTime;
Chiachang Wang0b34ae62020-05-21 07:03:23 +0000499 // The signal causing a data stall to be suspected. Reset to 0 after metrics are sent to statsd.
500 private @EvaluationType int mDataStallTypeToCollect;
Chiachang Wang813ee472019-05-23 16:29:30 +0800501 private boolean mAcceptPartialConnectivity = false;
502 private final EvaluationState mEvaluationState = new EvaluationState();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900503
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +0000504 private final boolean mPrivateIpNoInternetEnabled;
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +0000505
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900506 private final boolean mMetricsEnabled;
507
508 // The validation metrics are accessed by individual probe threads, and by the StateMachine
509 // thread. All accesses must be synchronized to make sure the StateMachine thread can see
510 // reports from all probes.
511 // TODO: as that most usage is in the StateMachine thread and probes only add their probe
512 // events, consider having probes return their stats to the StateMachine, and only access this
513 // member on the StateMachine thread without synchronization.
Frank Lie04f8b72020-06-22 12:45:27 +0000514 @GuardedBy("mNetworkValidationMetrics")
515 private final NetworkValidationMetrics mNetworkValidationMetrics =
516 new NetworkValidationMetrics();
517
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +0900518 private int getCallbackVersion(INetworkMonitorCallbacks cb) {
519 int version;
520 try {
521 version = cb.getInterfaceVersion();
522 } catch (RemoteException e) {
523 version = 0;
524 }
lucaslin2ce7dcc2019-10-22 16:59:39 +0800525 // The AIDL was freezed from Q beta 5 but it's unfreezing from R before releasing. In order
526 // to distinguish the behavior between R and Q beta 5 and before Q beta 5, add SDK and
527 // CODENAME check here. Basically, it's only expected to return 0 for Q beta 4 and below
528 // because the test result has changed.
529 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
530 && Build.VERSION.CODENAME.equals("REL")
531 && version == Build.VERSION_CODES.CUR_DEVELOPMENT) version = 0;
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +0900532 return version;
533 }
534
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900535 public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900536 SharedLog validationLog, @NonNull NetworkStackServiceManager serviceManager) {
537 this(context, cb, network, new IpConnectivityLog(), validationLog, serviceManager,
Chiachang Wang9b105a92020-03-30 09:12:37 +0000538 Dependencies.DEFAULT, getTcpSocketTrackerOrNull(context, network));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900539 }
540
541 @VisibleForTesting
Remi NGUYEN VANea9f7e32019-06-20 18:49:48 +0900542 public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
Remi NGUYEN VAN6e9fdbd2019-01-29 15:38:52 +0900543 IpConnectivityLog logger, SharedLog validationLogs,
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900544 @NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
Chiachang Wang9b105a92020-03-30 09:12:37 +0000545 @Nullable TcpSocketTracker tst) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900546 // Add suffix indicating which NetworkMonitor we're talking about.
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +0900547 super(TAG + "/" + network.toString());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900548
549 // Logs with a tag of the form given just above, e.g.
550 // <timestamp> 862 2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
551 setDbg(VDBG);
552
553 mContext = context;
554 mMetricsLog = logger;
555 mValidationLogs = validationLogs;
556 mCallback = cb;
Remi NGUYEN VANb4af1302019-06-11 16:17:46 +0900557 mCallbackVersion = getCallbackVersion(cb);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900558 mDependencies = deps;
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +0900559 mNetwork = network;
560 mCleartextDnsNetwork = deps.getPrivateDnsBypassNetwork(network);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900561 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
562 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
563 mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +0900564 mNotifier = serviceManager.getNotifier();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900565
566 // CHECKSTYLE:OFF IndentationCheck
567 addState(mDefaultState);
568 addState(mMaybeNotifyState, mDefaultState);
569 addState(mEvaluatingState, mMaybeNotifyState);
570 addState(mProbingState, mEvaluatingState);
571 addState(mWaitingForNextProbeState, mEvaluatingState);
572 addState(mCaptivePortalState, mMaybeNotifyState);
573 addState(mEvaluatingPrivateDnsState, mDefaultState);
Lucas Linfc5814c2020-05-05 08:15:19 +0000574 addState(mEvaluatingBandwidthState, mDefaultState);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900575 addState(mValidatedState, mDefaultState);
576 setInitialState(mDefaultState);
577 // CHECKSTYLE:ON IndentationCheck
578
579 mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +0000580 mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled();
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900581 mMetricsEnabled = deps.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
582 NetworkStackUtils.VALIDATION_METRICS_VERSION, true /* defaultEnabled */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900583 mUseHttps = getUseHttpsValidation();
584 mCaptivePortalUserAgent = getCaptivePortalUserAgent();
Chiachang Wanga64bb702020-03-31 07:50:59 +0000585 mCaptivePortalHttpsUrls = makeCaptivePortalHttpsUrls();
586 mCaptivePortalHttpUrls = makeCaptivePortalHttpUrls();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900587 mCaptivePortalFallbackUrls = makeCaptivePortalFallbackUrls();
588 mCaptivePortalFallbackSpecs = makeCaptivePortalFallbackProbeSpecs();
589 mRandom = deps.getRandom();
590 // TODO: Evaluate to move data stall configuration to a specific class.
591 mConsecutiveDnsTimeoutThreshold = getConsecutiveDnsTimeoutThreshold();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900592 mDataStallMinEvaluateTime = getDataStallMinEvaluateTime();
593 mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold();
Chiachang Wang9a87f802019-04-08 19:06:21 +0800594 mDataStallEvaluationType = getDataStallEvaluationType();
Chiachang Wang8e232042020-01-17 18:01:59 +0800595 mDnsStallDetector = initDnsStallDetectorIfRequired(mDataStallEvaluationType,
596 mConsecutiveDnsTimeoutThreshold);
Chiachang Wang4336b912019-11-26 15:39:22 +0800597 mTcpTracker = tst;
Lucas Linfc5814c2020-05-05 08:15:19 +0000598 // Read the configurations of evaluating network bandwidth.
599 mEvaluatingBandwidthUrl = getResStringConfig(mContext,
600 R.string.config_evaluating_bandwidth_url, null);
601 mMaxRetryTimerMs = getResIntConfig(mContext,
602 R.integer.config_evaluating_bandwidth_max_retry_timer_ms,
603 MAX_REEVALUATE_DELAY_MS);
604 mEvaluatingBandwidthTimeoutMs = getResIntConfig(mContext,
605 R.integer.config_evaluating_bandwidth_timeout_ms,
606 DEFAULT_EVALUATING_BANDWIDTH_TIMEOUT_MS);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900607
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900608 // Provide empty LinkProperties and NetworkCapabilities to make sure they are never null,
609 // even before notifyNetworkConnected.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900610 mLinkProperties = new LinkProperties();
Remi NGUYEN VAN6e9fdbd2019-01-29 15:38:52 +0900611 mNetworkCapabilities = new NetworkCapabilities(null);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900612 }
613
614 /**
lucaslind01ea622019-03-20 18:21:59 +0800615 * ConnectivityService notifies NetworkMonitor that the user already accepted partial
616 * connectivity previously, so NetworkMonitor can validate the network even if it has partial
617 * connectivity.
lucaslinb0573962019-03-12 13:08:03 +0800618 */
lucaslind01ea622019-03-20 18:21:59 +0800619 public void setAcceptPartialConnectivity() {
lucaslinb0573962019-03-12 13:08:03 +0800620 sendMessage(EVENT_ACCEPT_PARTIAL_CONNECTIVITY);
621 }
622
623 /**
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900624 * Request the NetworkMonitor to reevaluate the network.
625 */
626 public void forceReevaluation(int responsibleUid) {
627 sendMessage(CMD_FORCE_REEVALUATION, responsibleUid, 0);
628 }
629
630 /**
631 * Send a notification to NetworkMonitor indicating that there was a DNS query response event.
632 * @param returnCode the DNS return code of the response.
633 */
634 public void notifyDnsResponse(int returnCode) {
635 sendMessage(EVENT_DNS_NOTIFICATION, returnCode);
636 }
637
638 /**
639 * Send a notification to NetworkMonitor indicating that private DNS settings have changed.
640 * @param newCfg The new private DNS configuration.
641 */
642 public void notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg) {
643 // Cancel any outstanding resolutions.
644 removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
645 // Send the update to the proper thread.
646 sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
647 }
648
649 /**
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900650 * Send a notification to NetworkMonitor indicating that the network is now connected.
651 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900652 public void notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
653 sendMessage(CMD_NETWORK_CONNECTED, new Pair<>(
654 new LinkProperties(lp), new NetworkCapabilities(nc)));
655 }
656
657 private void updateConnectedNetworkAttributes(Message connectedMsg) {
658 final Pair<LinkProperties, NetworkCapabilities> attrs =
659 (Pair<LinkProperties, NetworkCapabilities>) connectedMsg.obj;
660 mLinkProperties = attrs.first;
661 mNetworkCapabilities = attrs.second;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900662 }
663
664 /**
665 * Send a notification to NetworkMonitor indicating that the network is now disconnected.
666 */
667 public void notifyNetworkDisconnected() {
668 sendMessage(CMD_NETWORK_DISCONNECTED);
669 }
670
671 /**
672 * Send a notification to NetworkMonitor indicating that link properties have changed.
673 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900674 public void notifyLinkPropertiesChanged(final LinkProperties lp) {
675 sendMessage(EVENT_LINK_PROPERTIES_CHANGED, new LinkProperties(lp));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900676 }
677
678 /**
679 * Send a notification to NetworkMonitor indicating that network capabilities have changed.
680 */
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900681 public void notifyNetworkCapabilitiesChanged(final NetworkCapabilities nc) {
682 sendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED, new NetworkCapabilities(nc));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900683 }
684
685 /**
686 * Request the captive portal application to be launched.
687 */
688 public void launchCaptivePortalApp() {
689 sendMessage(CMD_LAUNCH_CAPTIVE_PORTAL_APP);
690 }
691
Remi NGUYEN VANad99e542019-02-13 20:58:59 +0900692 /**
693 * Notify that the captive portal app was closed with the provided response code.
694 */
695 public void notifyCaptivePortalAppFinished(int response) {
696 sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response);
697 }
698
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900699 @Override
700 protected void log(String s) {
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +0900701 if (DBG) Log.d(TAG + "/" + mCleartextDnsNetwork.toString(), s);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900702 }
703
704 private void validationLog(int probeType, Object url, String msg) {
705 String probeName = ValidationProbeEvent.getProbeName(probeType);
706 validationLog(String.format("%s %s %s", probeName, url, msg));
707 }
708
709 private void validationLog(String s) {
710 if (DBG) log(s);
711 mValidationLogs.log(s);
712 }
713
714 private ValidationStage validationStage() {
715 return 0 == mValidations ? ValidationStage.FIRST_VALIDATION : ValidationStage.REVALIDATION;
716 }
717
718 private boolean isValidationRequired() {
Lorenzo Colittid4476892019-01-23 17:54:08 +0900719 return NetworkMonitorUtils.isValidationRequired(mNetworkCapabilities);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900720 }
721
Lorenzo Colitti6d39cb72019-03-22 00:28:28 +0900722 private boolean isPrivateDnsValidationRequired() {
723 return NetworkMonitorUtils.isPrivateDnsValidationRequired(mNetworkCapabilities);
724 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900725
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000726 private void notifyNetworkTested(NetworkTestResultParcelable result) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900727 try {
Cody Kestingccf13b52020-03-31 23:12:11 -0700728 if (mCallbackVersion <= 5) {
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000729 mCallback.notifyNetworkTested(
730 getLegacyTestResult(result.result, result.probesSucceeded),
731 result.redirectUrl);
Cody Kesting176bce72020-01-20 18:09:59 -0800732 } else {
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000733 mCallback.notifyNetworkTestedWithExtras(result);
Cody Kesting176bce72020-01-20 18:09:59 -0800734 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900735 } catch (RemoteException e) {
736 Log.e(TAG, "Error sending network test result", e);
737 }
738 }
739
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000740 /**
741 * Get the test result that was used as an int up to interface version 5.
742 *
743 * <p>For callback version < 3 (only used in Q beta preview builds), the int represented one of
744 * the NETWORK_TEST_RESULT_* constants.
745 *
746 * <p>Q released with version 3, which used a single int for both the evaluation result bitmask,
747 * and the probesSucceeded bitmask.
748 */
749 protected int getLegacyTestResult(int evaluationResult, int probesSucceeded) {
750 if (mCallbackVersion < 3) {
751 if ((evaluationResult & NETWORK_VALIDATION_RESULT_VALID) != 0) {
752 return NETWORK_TEST_RESULT_VALID;
753 }
754 if ((evaluationResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0) {
755 return NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY;
756 }
757 return NETWORK_TEST_RESULT_INVALID;
758 }
759
760 return evaluationResult | probesSucceeded;
761 }
762
lucaslin2ce7dcc2019-10-22 16:59:39 +0800763 private void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) {
764 try {
765 mCallback.notifyProbeStatusChanged(probesCompleted, probesSucceeded);
766 } catch (RemoteException e) {
767 Log.e(TAG, "Error sending probe status", e);
768 }
769 }
770
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900771 private void showProvisioningNotification(String action) {
772 try {
Remi NGUYEN VAN1e3eb372019-02-07 21:29:57 +0900773 mCallback.showProvisioningNotification(action, mContext.getPackageName());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900774 } catch (RemoteException e) {
775 Log.e(TAG, "Error showing provisioning notification", e);
776 }
777 }
778
779 private void hideProvisioningNotification() {
780 try {
781 mCallback.hideProvisioningNotification();
782 } catch (RemoteException e) {
783 Log.e(TAG, "Error hiding provisioning notification", e);
784 }
785 }
786
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000787 private void notifyDataStallSuspected(@NonNull DataStallReportParcelable p) {
Cody Kesting782cbfa2020-01-06 15:51:59 -0800788 try {
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +0000789 mCallback.notifyDataStallSuspected(p);
Cody Kesting782cbfa2020-01-06 15:51:59 -0800790 } catch (RemoteException e) {
791 Log.e(TAG, "Error sending notification for suspected data stall", e);
792 }
793 }
794
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900795 private void startMetricsCollection() {
796 if (!mMetricsEnabled) return;
797 try {
798 synchronized (mNetworkValidationMetrics) {
799 mNetworkValidationMetrics.startCollection(mNetworkCapabilities);
800 }
801 } catch (Exception e) {
802 Log.wtf(TAG, "Error resetting validation metrics", e);
Frank Lie04f8b72020-06-22 12:45:27 +0000803 }
804 }
805
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900806 private void recordProbeEventMetrics(ProbeType type, long latencyMicros, ProbeResult result,
Frank Lie04f8b72020-06-22 12:45:27 +0000807 CaptivePortalDataShim capportData) {
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900808 if (!mMetricsEnabled) return;
809 try {
810 synchronized (mNetworkValidationMetrics) {
811 mNetworkValidationMetrics.addProbeEvent(type, latencyMicros, result, capportData);
812 }
813 } catch (Exception e) {
814 Log.wtf(TAG, "Error recording probe event", e);
Frank Lie04f8b72020-06-22 12:45:27 +0000815 }
816 }
817
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900818 private void recordValidationResult(int result, String redirectUrl) {
819 if (!mMetricsEnabled) return;
820 try {
821 synchronized (mNetworkValidationMetrics) {
822 mNetworkValidationMetrics.setValidationResult(result, redirectUrl);
823 }
824 } catch (Exception e) {
825 Log.wtf(TAG, "Error recording validation result", e);
Frank Lie04f8b72020-06-22 12:45:27 +0000826 }
827 }
828
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900829 private void maybeStopCollectionAndSendMetrics() {
830 if (!mMetricsEnabled) return;
831 try {
832 synchronized (mNetworkValidationMetrics) {
833 mNetworkValidationMetrics.maybeStopCollectionAndSend();
834 }
835 } catch (Exception e) {
836 Log.wtf(TAG, "Error sending validation stats", e);
Frank Lie04f8b72020-06-22 12:45:27 +0000837 }
838 }
839
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900840 // DefaultState is the parent of all States. It exists only to handle CMD_* messages but
841 // does not entail any real state (hence no enter() or exit() routines).
842 private class DefaultState extends State {
843 @Override
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900844 public boolean processMessage(Message message) {
845 switch (message.what) {
846 case CMD_NETWORK_CONNECTED:
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900847 updateConnectedNetworkAttributes(message);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900848 logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
849 transitionTo(mEvaluatingState);
850 return HANDLED;
851 case CMD_NETWORK_DISCONNECTED:
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +0900852 maybeStopCollectionAndSendMetrics();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900853 logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900854 quit();
855 return HANDLED;
856 case CMD_FORCE_REEVALUATION:
857 case CMD_CAPTIVE_PORTAL_RECHECK:
Remi NGUYEN VAN9572aa12020-06-09 11:27:15 +0000858 if (getCurrentState() == mDefaultState) {
859 // Before receiving CMD_NETWORK_CONNECTED (when still in mDefaultState),
860 // requests to reevaluate are not valid: drop them.
861 return HANDLED;
862 }
Chiachang Wang8e232042020-01-17 18:01:59 +0800863 String msg = "Forcing reevaluation for UID " + message.arg1;
864 final DnsStallDetector dsd = getDnsStallDetector();
865 if (dsd != null) {
866 msg += ". Dns signal count: " + dsd.getConsecutiveTimeoutCount();
867 }
868 validationLog(msg);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900869 mUidResponsibleForReeval = message.arg1;
870 transitionTo(mEvaluatingState);
871 return HANDLED;
872 case CMD_CAPTIVE_PORTAL_APP_FINISHED:
873 log("CaptivePortal App responded with " + message.arg1);
874
875 // If the user has seen and acted on a captive portal notification, and the
876 // captive portal app is now closed, disable HTTPS probes. This avoids the
877 // following pathological situation:
878 //
879 // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out.
880 // 2. User opens the app and logs into the captive portal.
881 // 3. HTTP starts working, but HTTPS still doesn't work for some other reason -
882 // perhaps due to the network blocking HTTPS?
883 //
884 // In this case, we'll fail to validate the network even after the app is
885 // dismissed. There is now no way to use this network, because the app is now
886 // gone, so the user cannot select "Use this network as is".
887 mUseHttps = false;
888
889 switch (message.arg1) {
890 case APP_RETURN_DISMISSED:
891 sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
892 break;
893 case APP_RETURN_WANTED_AS_IS:
894 mDontDisplaySigninNotification = true;
Lucas Linfc5814c2020-05-05 08:15:19 +0000895 // If the user wants to use this network anyway, there is no need to
896 // perform the bandwidth check even if configured.
897 mIsBandwidthCheckPassedOrIgnored = true;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900898 // TODO: Distinguish this from a network that actually validates.
899 // Displaying the "x" on the system UI icon may still be a good idea.
900 transitionTo(mEvaluatingPrivateDnsState);
901 break;
902 case APP_RETURN_UNWANTED:
903 mDontDisplaySigninNotification = true;
904 mUserDoesNotWant = true;
Chiachang Wang813ee472019-05-23 16:29:30 +0800905 mEvaluationState.reportEvaluationResult(
906 NETWORK_VALIDATION_RESULT_INVALID, null);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900907 // TODO: Should teardown network.
908 mUidResponsibleForReeval = 0;
909 transitionTo(mEvaluatingState);
910 break;
911 }
912 return HANDLED;
913 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
914 final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
Lorenzo Colitti6d39cb72019-03-22 00:28:28 +0900915 if (!isPrivateDnsValidationRequired() || cfg == null || !cfg.inStrictMode()) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900916 // No DNS resolution required.
917 //
918 // We don't force any validation in opportunistic mode
919 // here. Opportunistic mode nameservers are validated
920 // separately within netd.
921 //
922 // Reset Private DNS settings state.
923 mPrivateDnsProviderHostname = "";
924 break;
925 }
926
927 mPrivateDnsProviderHostname = cfg.hostname;
928
929 // DNS resolutions via Private DNS strict mode block for a
930 // few seconds (~4.2) checking for any IP addresses to
931 // arrive and validate. Initiating a (re)evaluation now
932 // should not significantly alter the validation outcome.
933 //
934 // No matter what: enqueue a validation request; one of
935 // three things can happen with this request:
936 // [1] ignored (EvaluatingState or CaptivePortalState)
937 // [2] transition to EvaluatingPrivateDnsState
938 // (DefaultState and ValidatedState)
939 // [3] handled (EvaluatingPrivateDnsState)
940 //
941 // The Private DNS configuration to be evaluated will:
942 // [1] be skipped (not in strict mode), or
943 // [2] validate (huzzah), or
944 // [3] encounter some problem (invalid hostname,
945 // no resolved IP addresses, IPs unreachable,
946 // port 853 unreachable, port 853 is not running a
947 // DNS-over-TLS server, et cetera).
Chiachang Wanged5f5192019-10-23 21:23:36 +0800948 // Cancel any outstanding CMD_EVALUATE_PRIVATE_DNS.
949 removeMessages(CMD_EVALUATE_PRIVATE_DNS);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900950 sendMessage(CMD_EVALUATE_PRIVATE_DNS);
951 break;
952 }
953 case EVENT_DNS_NOTIFICATION:
Chiachang Wang8e232042020-01-17 18:01:59 +0800954 final DnsStallDetector detector = getDnsStallDetector();
955 if (detector != null) {
956 detector.accumulateConsecutiveDnsTimeoutCount(message.arg1);
957 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900958 break;
lucaslind01ea622019-03-20 18:21:59 +0800959 // Set mAcceptPartialConnectivity to true and if network start evaluating or
960 // re-evaluating and get the result of partial connectivity, ProbingState will
961 // disable HTTPS probe and transition to EvaluatingPrivateDnsState.
lucaslinb0573962019-03-12 13:08:03 +0800962 case EVENT_ACCEPT_PARTIAL_CONNECTIVITY:
Chiachang Wang813ee472019-05-23 16:29:30 +0800963 maybeDisableHttpsProbing(true /* acceptPartial */);
lucaslinb0573962019-03-12 13:08:03 +0800964 break;
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900965 case EVENT_LINK_PROPERTIES_CHANGED:
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900966 final Uri oldCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900967 mLinkProperties = (LinkProperties) message.obj;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +0900968 final Uri newCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
969 if (!Objects.equals(oldCapportUrl, newCapportUrl)) {
970 sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
971 }
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +0900972 break;
973 case EVENT_NETWORK_CAPABILITIES_CHANGED:
974 mNetworkCapabilities = (NetworkCapabilities) message.obj;
975 break;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +0900976 default:
977 break;
978 }
979 return HANDLED;
980 }
981 }
982
983 // Being in the ValidatedState State indicates a Network is:
984 // - Successfully validated, or
985 // - Wanted "as is" by the user, or
986 // - Does not satisfy the default NetworkRequest and so validation has been skipped.
987 private class ValidatedState extends State {
988 @Override
989 public void enter() {
990 maybeLogEvaluationResult(
991 networkEventType(validationStage(), EvaluationResult.VALIDATED));
Chiachang Wangf1bf7e72019-05-24 11:20:47 +0800992 // If the user has accepted partial connectivity and HTTPS probing is disabled, then
993 // mark the network as validated and partial so that settings can keep informing the
994 // user that the connection is limited.
Chiachang Wang813ee472019-05-23 16:29:30 +0800995 int result = NETWORK_VALIDATION_RESULT_VALID;
996 if (!mUseHttps && mAcceptPartialConnectivity) {
997 result |= NETWORK_VALIDATION_RESULT_PARTIAL;
998 }
999 mEvaluationState.reportEvaluationResult(result, null /* redirectUrl */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001000 mValidations++;
Chiachang Wangd740dc32020-01-14 20:49:17 +08001001 initSocketTrackingIfRequired();
1002 // start periodical polling.
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001003 sendTcpPollingEvent();
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001004 maybeStopCollectionAndSendMetrics();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001005 }
1006
Chiachang Wangd740dc32020-01-14 20:49:17 +08001007 private void initSocketTrackingIfRequired() {
1008 if (!isValidationRequired()) return;
1009
1010 final TcpSocketTracker tst = getTcpSocketTracker();
1011 if (tst != null) {
1012 tst.pollSocketsInfo();
1013 }
1014 }
1015
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001016 @Override
1017 public boolean processMessage(Message message) {
1018 switch (message.what) {
1019 case CMD_NETWORK_CONNECTED:
Remi NGUYEN VANdc6e6402019-03-27 15:42:53 +09001020 updateConnectedNetworkAttributes(message);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001021 transitionTo(mValidatedState);
1022 break;
1023 case CMD_EVALUATE_PRIVATE_DNS:
Frank Lie04f8b72020-06-22 12:45:27 +00001024 // TODO: this causes reevaluation of a single probe that is not counted in
1025 // metrics. Add support for such reevaluation probes in metrics, and log them
1026 // separately.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001027 transitionTo(mEvaluatingPrivateDnsState);
1028 break;
1029 case EVENT_DNS_NOTIFICATION:
Chiachang Wang8e232042020-01-17 18:01:59 +08001030 final DnsStallDetector dsd = getDnsStallDetector();
1031 if (dsd == null) break;
1032
1033 dsd.accumulateConsecutiveDnsTimeoutCount(message.arg1);
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001034 if (evaluateDataStall()) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001035 transitionTo(mEvaluatingState);
1036 }
1037 break;
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001038 case EVENT_POLL_TCPINFO:
Chiachang Wange797c9b2019-11-28 14:18:47 +08001039 final TcpSocketTracker tst = getTcpSocketTracker();
1040 if (tst == null) break;
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001041 // Transit if retrieve socket info is succeeded and suspected as a stall.
Chiachang Wange797c9b2019-11-28 14:18:47 +08001042 if (tst.pollSocketsInfo() && evaluateDataStall()) {
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001043 transitionTo(mEvaluatingState);
1044 } else {
1045 sendTcpPollingEvent();
1046 }
1047 break;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001048 default:
1049 return NOT_HANDLED;
1050 }
1051 return HANDLED;
1052 }
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001053
1054 boolean evaluateDataStall() {
1055 if (isDataStall()) {
Chiachang Wanga5716bf2019-11-20 16:13:07 +08001056 validationLog("Suspecting data stall, reevaluate");
1057 return true;
1058 }
1059 return false;
1060 }
1061
1062 @Override
1063 public void exit() {
1064 // Not useful for non-ValidatedState.
1065 removeMessages(EVENT_POLL_TCPINFO);
1066 }
1067 }
1068
1069 @VisibleForTesting
1070 void sendTcpPollingEvent() {
1071 if (isValidationRequired()) {
1072 sendMessageDelayed(EVENT_POLL_TCPINFO, getTcpPollingInterval());
1073 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001074 }
1075
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001076 private void maybeWriteDataStallStats(@NonNull final CaptivePortalProbeResult result) {
1077 if (mDataStallTypeToCollect == DATA_STALL_EVALUATION_TYPE_NONE) return;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001078 /*
1079 * Collect data stall detection level information for each transport type. Collect type
1080 * specific information for cellular and wifi only currently. Generate
1081 * DataStallDetectionStats for each transport type. E.g., if a network supports both
1082 * TRANSPORT_WIFI and TRANSPORT_VPN, two DataStallDetectionStats will be generated.
1083 */
1084 final int[] transports = mNetworkCapabilities.getTransportTypes();
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001085 for (int i = 0; i < transports.length; i++) {
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001086 final DataStallDetectionStats stats =
1087 buildDataStallDetectionStats(transports[i], mDataStallTypeToCollect);
Chiachang Wang9b105a92020-03-30 09:12:37 +00001088 mDependencies.writeDataStallDetectionStats(stats, result);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001089 }
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001090 mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_NONE;
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001091 }
1092
1093 @VisibleForTesting
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001094 protected DataStallDetectionStats buildDataStallDetectionStats(int transport,
1095 @EvaluationType int evaluationType) {
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001096 final DataStallDetectionStats.Builder stats = new DataStallDetectionStats.Builder();
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001097 if (VDBG_STALL) {
1098 log("collectDataStallMetrics: type=" + transport + ", evaluation=" + evaluationType);
1099 }
1100 stats.setEvaluationType(evaluationType);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001101 stats.setNetworkType(transport);
1102 switch (transport) {
1103 case NetworkCapabilities.TRANSPORT_WIFI:
1104 // TODO: Update it if status query in dual wifi is supported.
1105 final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
1106 stats.setWiFiData(wifiInfo);
1107 break;
1108 case NetworkCapabilities.TRANSPORT_CELLULAR:
1109 final boolean isRoaming = !mNetworkCapabilities.hasCapability(
1110 NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
1111 final SignalStrength ss = mTelephonyManager.getSignalStrength();
1112 // TODO(b/120452078): Support multi-sim.
1113 stats.setCellData(
1114 mTelephonyManager.getDataNetworkType(),
1115 isRoaming,
1116 mTelephonyManager.getNetworkOperator(),
1117 mTelephonyManager.getSimOperator(),
1118 (ss != null)
1119 ? ss.getLevel() : CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
1120 break;
1121 default:
1122 // No transport type specific information for the other types.
1123 break;
1124 }
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001125
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001126 addDnsEvents(stats);
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001127 addTcpStats(stats);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001128
1129 return stats.build();
1130 }
1131
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001132 private void addTcpStats(@NonNull final DataStallDetectionStats.Builder stats) {
1133 final TcpSocketTracker tst = getTcpSocketTracker();
1134 if (tst == null) return;
1135
1136 stats.setTcpSentSinceLastRecv(tst.getSentSinceLastRecv());
1137 stats.setTcpFailRate(tst.getLatestPacketFailPercentage());
1138 }
1139
Chiachang Wangcaa35202019-02-26 11:32:18 +08001140 @VisibleForTesting
1141 protected void addDnsEvents(@NonNull final DataStallDetectionStats.Builder stats) {
Chiachang Wang8e232042020-01-17 18:01:59 +08001142 final DnsStallDetector dsd = getDnsStallDetector();
1143 if (dsd == null) return;
1144
1145 final int size = dsd.mResultIndices.size();
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001146 for (int i = 1; i <= DEFAULT_DNS_LOG_SIZE && i <= size; i++) {
Chiachang Wang8e232042020-01-17 18:01:59 +08001147 final int index = dsd.mResultIndices.indexOf(size - i);
1148 stats.addDnsEvent(dsd.mDnsEvents[index].mReturnCode, dsd.mDnsEvents[index].mTimeStamp);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001149 }
1150 }
1151
1152
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001153 // Being in the MaybeNotifyState State indicates the user may have been notified that sign-in
1154 // is required. This State takes care to clear the notification upon exit from the State.
1155 private class MaybeNotifyState extends State {
1156 @Override
1157 public boolean processMessage(Message message) {
1158 switch (message.what) {
1159 case CMD_LAUNCH_CAPTIVE_PORTAL_APP:
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +09001160 final Bundle appExtras = new Bundle();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001161 // OneAddressPerFamilyNetwork is not parcelable across processes.
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09001162 final Network network = new Network(mCleartextDnsNetwork);
Remi NGUYEN VANad99e542019-02-13 20:58:59 +09001163 appExtras.putParcelable(ConnectivityManager.EXTRA_NETWORK, network);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001164 final CaptivePortalProbeResult probeRes = mLastPortalProbeResult;
Automerger Merge Workerfaac06e2020-03-09 09:23:38 +00001165 // Use redirect URL from AP if exists.
1166 final String portalUrl =
Chiachang Wangffaef492020-05-29 22:38:12 +00001167 (useRedirectUrlForPortal() && makeURL(probeRes.redirectUrl) != null)
Automerger Merge Workerfaac06e2020-03-09 09:23:38 +00001168 ? probeRes.redirectUrl : probeRes.detectUrl;
1169 appExtras.putString(EXTRA_CAPTIVE_PORTAL_URL, portalUrl);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001170 if (probeRes.probeSpec != null) {
1171 final String encodedSpec = probeRes.probeSpec.getEncodedSpec();
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +09001172 appExtras.putString(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC, encodedSpec);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001173 }
Remi NGUYEN VAN56fcae32019-02-04 11:32:20 +09001174 appExtras.putString(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001175 mCaptivePortalUserAgent);
Remi NGUYEN VAN89cd0262019-12-29 22:46:00 +09001176 if (mNotifier != null) {
1177 mNotifier.notifyCaptivePortalValidationPending(network);
1178 }
Remi NGUYEN VANad99e542019-02-13 20:58:59 +09001179 mCm.startCaptivePortalApp(network, appExtras);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001180 return HANDLED;
1181 default:
1182 return NOT_HANDLED;
1183 }
1184 }
1185
Automerger Merge Workerfaac06e2020-03-09 09:23:38 +00001186 private boolean useRedirectUrlForPortal() {
1187 // It must match the conditions in CaptivePortalLogin in which the redirect URL is not
1188 // used to validate that the portal is gone.
1189 final boolean aboveQ =
1190 ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
1191 return aboveQ && mDependencies.isFeatureEnabled(mContext, NAMESPACE_CONNECTIVITY,
1192 DISMISS_PORTAL_IN_VALIDATED_NETWORK, aboveQ /* defaultEnabled */);
1193 }
1194
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001195 @Override
1196 public void exit() {
Lorenzo Colitti687423b2019-04-08 17:14:35 +09001197 if (mLaunchCaptivePortalAppBroadcastReceiver != null) {
1198 mContext.unregisterReceiver(mLaunchCaptivePortalAppBroadcastReceiver);
1199 mLaunchCaptivePortalAppBroadcastReceiver = null;
1200 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001201 hideProvisioningNotification();
1202 }
1203 }
1204
1205 // Being in the EvaluatingState State indicates the Network is being evaluated for internet
1206 // connectivity, or that the user has indicated that this network is unwanted.
1207 private class EvaluatingState extends State {
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09001208 private Uri mEvaluatingCapportUrl;
1209
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001210 @Override
1211 public void enter() {
1212 // If we have already started to track time spent in EvaluatingState
1213 // don't reset the timer due simply to, say, commands or events that
1214 // cause us to exit and re-enter EvaluatingState.
1215 if (!mEvaluationTimer.isStarted()) {
1216 mEvaluationTimer.start();
1217 }
1218 sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
1219 if (mUidResponsibleForReeval != INVALID_UID) {
1220 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
1221 mUidResponsibleForReeval = INVALID_UID;
1222 }
1223 mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
1224 mEvaluateAttempts = 0;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09001225 mEvaluatingCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
Chiachang Wang813ee472019-05-23 16:29:30 +08001226 // Reset all current probe results to zero, but retain current validation state until
1227 // validation succeeds or fails.
1228 mEvaluationState.clearProbeResults();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001229 }
1230
1231 @Override
1232 public boolean processMessage(Message message) {
1233 switch (message.what) {
1234 case CMD_REEVALUATE:
1235 if (message.arg1 != mReevaluateToken || mUserDoesNotWant) {
1236 return HANDLED;
1237 }
1238 // Don't bother validating networks that don't satisfy the default request.
1239 // This includes:
1240 // - VPNs which can be considered explicitly desired by the user and the
1241 // user's desire trumps whether the network validates.
1242 // - Networks that don't provide Internet access. It's unclear how to
1243 // validate such networks.
1244 // - Untrusted networks. It's unsafe to prompt the user to sign-in to
1245 // such networks and the user didn't express interest in connecting to
1246 // such networks (an app did) so the user may be unhappily surprised when
1247 // asked to sign-in to a network they didn't want to connect to in the
1248 // first place. Validation could be done to adjust the network scores
1249 // however these networks are app-requested and may not be intended for
1250 // general usage, in which case general validation may not be an accurate
1251 // measure of the network's quality. Only the app knows how to evaluate
1252 // the network so don't bother validating here. Furthermore sending HTTP
1253 // packets over the network may be undesirable, for example an extremely
1254 // expensive metered network, or unwanted leaking of the User Agent string.
Lorenzo Colitti6d39cb72019-03-22 00:28:28 +09001255 //
1256 // On networks that need to support private DNS in strict mode (e.g., VPNs, but
1257 // not networks that don't provide Internet access), we still need to perform
1258 // private DNS server resolution.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001259 if (!isValidationRequired()) {
Lorenzo Colitti6d39cb72019-03-22 00:28:28 +09001260 if (isPrivateDnsValidationRequired()) {
1261 validationLog("Network would not satisfy default request, "
1262 + "resolving private DNS");
1263 transitionTo(mEvaluatingPrivateDnsState);
1264 } else {
1265 validationLog("Network would not satisfy default request, "
1266 + "not validating");
1267 transitionTo(mValidatedState);
1268 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001269 return HANDLED;
1270 }
1271 mEvaluateAttempts++;
1272
1273 transitionTo(mProbingState);
1274 return HANDLED;
1275 case CMD_FORCE_REEVALUATION:
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09001276 // The evaluation process restarts via EvaluatingState#enter.
1277 return shouldAcceptForceRevalidation() ? NOT_HANDLED : HANDLED;
lucaslind01ea622019-03-20 18:21:59 +08001278 // Disable HTTPS probe and transition to EvaluatingPrivateDnsState because:
1279 // 1. Network is connected and finish the network validation.
1280 // 2. NetworkMonitor detects network is partial connectivity and user accepts it.
1281 case EVENT_ACCEPT_PARTIAL_CONNECTIVITY:
Chiachang Wang813ee472019-05-23 16:29:30 +08001282 maybeDisableHttpsProbing(true /* acceptPartial */);
lucaslind01ea622019-03-20 18:21:59 +08001283 transitionTo(mEvaluatingPrivateDnsState);
1284 return HANDLED;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001285 default:
1286 return NOT_HANDLED;
1287 }
1288 }
1289
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09001290 private boolean shouldAcceptForceRevalidation() {
1291 // If the captive portal URL has changed since the last evaluation attempt, always
1292 // revalidate. Otherwise, ignore any re-evaluation requests before
1293 // IGNORE_REEVALUATE_ATTEMPTS are made.
1294 return mEvaluateAttempts >= IGNORE_REEVALUATE_ATTEMPTS
1295 || !Objects.equals(
1296 mEvaluatingCapportUrl, getCaptivePortalApiUrl(mLinkProperties));
1297 }
1298
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001299 @Override
1300 public void exit() {
1301 TrafficStats.clearThreadStatsUid();
1302 }
1303 }
1304
1305 // BroadcastReceiver that waits for a particular Intent and then posts a message.
1306 private class CustomIntentReceiver extends BroadcastReceiver {
1307 private final int mToken;
1308 private final int mWhat;
1309 private final String mAction;
1310 CustomIntentReceiver(String action, int token, int what) {
1311 mToken = token;
1312 mWhat = what;
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09001313 mAction = action + "_" + mCleartextDnsNetwork.getNetworkHandle() + "_" + token;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001314 mContext.registerReceiver(this, new IntentFilter(mAction));
1315 }
1316 public PendingIntent getPendingIntent() {
1317 final Intent intent = new Intent(mAction);
1318 intent.setPackage(mContext.getPackageName());
1319 return PendingIntent.getBroadcast(mContext, 0, intent, 0);
1320 }
1321 @Override
1322 public void onReceive(Context context, Intent intent) {
1323 if (intent.getAction().equals(mAction)) sendMessage(obtainMessage(mWhat, mToken));
1324 }
1325 }
1326
1327 // Being in the CaptivePortalState State indicates a captive portal was detected and the user
1328 // has been shown a notification to sign-in.
1329 private class CaptivePortalState extends State {
1330 private static final String ACTION_LAUNCH_CAPTIVE_PORTAL_APP =
1331 "android.net.netmon.launchCaptivePortalApp";
1332
1333 @Override
1334 public void enter() {
1335 maybeLogEvaluationResult(
1336 networkEventType(validationStage(), EvaluationResult.CAPTIVE_PORTAL));
1337 // Don't annoy user with sign-in notifications.
1338 if (mDontDisplaySigninNotification) return;
1339 // Create a CustomIntentReceiver that sends us a
1340 // CMD_LAUNCH_CAPTIVE_PORTAL_APP message when the user
1341 // touches the notification.
1342 if (mLaunchCaptivePortalAppBroadcastReceiver == null) {
1343 // Wait for result.
1344 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver(
1345 ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(),
1346 CMD_LAUNCH_CAPTIVE_PORTAL_APP);
lucaslin12b92a42019-04-08 10:52:46 +08001347 // Display the sign in notification.
1348 // Only do this once for every time we enter MaybeNotifyState. b/122164725
1349 showProvisioningNotification(mLaunchCaptivePortalAppBroadcastReceiver.mAction);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001350 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001351 // Retest for captive portal occasionally.
1352 sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
1353 CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
1354 mValidations++;
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001355 maybeStopCollectionAndSendMetrics();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001356 }
1357
1358 @Override
1359 public void exit() {
1360 removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
1361 }
1362 }
1363
1364 private class EvaluatingPrivateDnsState extends State {
1365 private int mPrivateDnsReevalDelayMs;
1366 private PrivateDnsConfig mPrivateDnsConfig;
1367
1368 @Override
1369 public void enter() {
1370 mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
1371 mPrivateDnsConfig = null;
1372 sendMessage(CMD_EVALUATE_PRIVATE_DNS);
1373 }
1374
1375 @Override
1376 public boolean processMessage(Message msg) {
1377 switch (msg.what) {
1378 case CMD_EVALUATE_PRIVATE_DNS:
1379 if (inStrictMode()) {
1380 if (!isStrictModeHostnameResolved()) {
1381 resolveStrictModeHostname();
1382
1383 if (isStrictModeHostnameResolved()) {
1384 notifyPrivateDnsConfigResolved();
1385 } else {
1386 handlePrivateDnsEvaluationFailure();
Frank Lie04f8b72020-06-22 12:45:27 +00001387 // The private DNS probe fails-fast if the server hostname cannot
1388 // be resolved. Record it as a failure with zero latency.
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001389 // TODO: refactor this together with the probe recorded in
1390 // sendPrivateDnsProbe, so logging is symmetric / easier to follow.
1391 recordProbeEventMetrics(ProbeType.PT_PRIVDNS, 0 /* latency */,
Frank Lie04f8b72020-06-22 12:45:27 +00001392 ProbeResult.PR_FAILURE, null /* capportData */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001393 break;
1394 }
1395 }
1396
1397 // Look up a one-time hostname, to bypass caching.
1398 //
1399 // Note that this will race with ConnectivityService
1400 // code programming the DNS-over-TLS server IP addresses
1401 // into netd (if invoked, above). If netd doesn't know
1402 // the IP addresses yet, or if the connections to the IP
1403 // addresses haven't yet been validated, netd will block
1404 // for up to a few seconds before failing the lookup.
1405 if (!sendPrivateDnsProbe()) {
1406 handlePrivateDnsEvaluationFailure();
1407 break;
1408 }
lucaslin2ce7dcc2019-10-22 16:59:39 +08001409 handlePrivateDnsEvaluationSuccess();
1410 } else {
1411 mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001412 }
1413
Lucas Linfc5814c2020-05-05 08:15:19 +00001414 if (needEvaluatingBandwidth()) {
1415 transitionTo(mEvaluatingBandwidthState);
1416 } else {
1417 // All good!
1418 transitionTo(mValidatedState);
1419 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001420 break;
Chiachang Wanged5f5192019-10-23 21:23:36 +08001421 case CMD_PRIVATE_DNS_SETTINGS_CHANGED:
1422 // When settings change the reevaluation timer must be reset.
1423 mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
1424 // Let the message bubble up and be handled by parent states as usual.
1425 return NOT_HANDLED;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001426 default:
1427 return NOT_HANDLED;
1428 }
1429 return HANDLED;
1430 }
1431
1432 private boolean inStrictMode() {
1433 return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
1434 }
1435
1436 private boolean isStrictModeHostnameResolved() {
1437 return (mPrivateDnsConfig != null)
1438 && mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname)
1439 && (mPrivateDnsConfig.ips.length > 0);
1440 }
1441
1442 private void resolveStrictModeHostname() {
1443 try {
1444 // Do a blocking DNS resolution using the network-assigned nameservers.
Chiachang Wang1c67f4e2019-05-09 21:28:47 +08001445 final InetAddress[] ips = DnsUtils.getAllByName(mDependencies.getDnsResolver(),
Chiachang Wangddb7da62019-06-03 15:50:53 +08001446 mCleartextDnsNetwork, mPrivateDnsProviderHostname, getDnsProbeTimeout(),
1447 str -> validationLog("Strict mode hostname resolution " + str));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001448 mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001449 } catch (UnknownHostException uhe) {
1450 mPrivateDnsConfig = null;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001451 }
1452 }
1453
1454 private void notifyPrivateDnsConfigResolved() {
1455 try {
1456 mCallback.notifyPrivateDnsConfigResolved(mPrivateDnsConfig.toParcel());
1457 } catch (RemoteException e) {
1458 Log.e(TAG, "Error sending private DNS config resolved notification", e);
1459 }
1460 }
1461
lucaslin2ce7dcc2019-10-22 16:59:39 +08001462 private void handlePrivateDnsEvaluationSuccess() {
1463 mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
1464 true /* succeeded */);
1465 }
1466
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001467 private void handlePrivateDnsEvaluationFailure() {
lucaslin2ce7dcc2019-10-22 16:59:39 +08001468 mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
1469 false /* succeeded */);
Chiachang Wang813ee472019-05-23 16:29:30 +08001470 mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
1471 null /* redirectUrl */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001472 // Queue up a re-evaluation with backoff.
1473 //
1474 // TODO: Consider abandoning this state after a few attempts and
1475 // transitioning back to EvaluatingState, to perhaps give ourselves
1476 // the opportunity to (re)detect a captive portal or something.
Chiachang Wangf1bf7e72019-05-24 11:20:47 +08001477 //
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001478 sendMessageDelayed(CMD_EVALUATE_PRIVATE_DNS, mPrivateDnsReevalDelayMs);
1479 mPrivateDnsReevalDelayMs *= 2;
1480 if (mPrivateDnsReevalDelayMs > MAX_REEVALUATE_DELAY_MS) {
1481 mPrivateDnsReevalDelayMs = MAX_REEVALUATE_DELAY_MS;
1482 }
1483 }
1484
1485 private boolean sendPrivateDnsProbe() {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001486 final String host = UUID.randomUUID().toString().substring(0, 8)
Chiachang Wangeb619222019-07-03 20:52:08 +08001487 + PRIVATE_DNS_PROBE_HOST_SUFFIX;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001488 final Stopwatch watch = new Stopwatch().start();
Chiachang Wang813ee472019-05-23 16:29:30 +08001489 boolean success = false;
1490 long time;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001491 try {
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09001492 final InetAddress[] ips = mNetwork.getAllByName(host);
Chiachang Wang813ee472019-05-23 16:29:30 +08001493 time = watch.stop();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001494 final String strIps = Arrays.toString(ips);
Chiachang Wang813ee472019-05-23 16:29:30 +08001495 success = (ips != null && ips.length > 0);
Frank Lifa914142020-05-18 04:48:03 +00001496 validationLog(PROBE_PRIVDNS, host, String.format("%dus: %s", time, strIps));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001497 } catch (UnknownHostException uhe) {
Chiachang Wang813ee472019-05-23 16:29:30 +08001498 time = watch.stop();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001499 validationLog(PROBE_PRIVDNS, host,
Frank Lifa914142020-05-18 04:48:03 +00001500 String.format("%dus - Error: %s", time, uhe.getMessage()));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001501 }
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001502 recordProbeEventMetrics(ProbeType.PT_PRIVDNS, time, success ? ProbeResult.PR_SUCCESS :
Frank Lie04f8b72020-06-22 12:45:27 +00001503 ProbeResult.PR_FAILURE, null /* capportData */);
Chiachang Wang813ee472019-05-23 16:29:30 +08001504 logValidationProbe(time, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE);
Chiachang Wang813ee472019-05-23 16:29:30 +08001505 return success;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001506 }
1507 }
1508
1509 private class ProbingState extends State {
1510 private Thread mThread;
1511
1512 @Override
1513 public void enter() {
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001514 // When starting a full probe cycle here, record any pending stats (for example if
1515 // CMD_FORCE_REEVALUATE was called before evaluation finished, as can happen in
1516 // EvaluatingPrivateDnsState).
1517 maybeStopCollectionAndSendMetrics();
1518 // Restart the metrics collection timers. Metrics will be stopped and sent when the
1519 // validation attempt finishes (as success, failure or portal), or if it is interrupted
1520 // (by being restarted or if NetworkMonitor stops).
1521 startMetricsCollection();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001522 if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
1523 //Don't continue to blame UID forever.
1524 TrafficStats.clearThreadStatsUid();
1525 }
1526
1527 final int token = ++mProbeToken;
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00001528 final EvaluationThreadDeps deps = new EvaluationThreadDeps(mNetworkCapabilities);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001529 mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00001530 isCaptivePortal(deps))));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001531 mThread.start();
1532 }
1533
1534 @Override
1535 public boolean processMessage(Message message) {
1536 switch (message.what) {
1537 case CMD_PROBE_COMPLETE:
1538 // Ensure that CMD_PROBE_COMPLETE from stale threads are ignored.
1539 if (message.arg1 != mProbeToken) {
1540 return HANDLED;
1541 }
1542
1543 final CaptivePortalProbeResult probeResult =
1544 (CaptivePortalProbeResult) message.obj;
1545 mLastProbeTime = SystemClock.elapsedRealtime();
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001546
Chiachang Wang0b34ae62020-05-21 07:03:23 +00001547 maybeWriteDataStallStats(probeResult);
Chiachang Wang8b5f84a2019-02-22 11:13:07 +08001548
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001549 if (probeResult.isSuccessful()) {
1550 // Transit EvaluatingPrivateDnsState to get to Validated
1551 // state (even if no Private DNS validation required).
1552 transitionTo(mEvaluatingPrivateDnsState);
1553 } else if (probeResult.isPortal()) {
Chiachang Wang813ee472019-05-23 16:29:30 +08001554 mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
1555 probeResult.redirectUrl);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001556 mLastPortalProbeResult = probeResult;
1557 transitionTo(mCaptivePortalState);
lucaslinb0573962019-03-12 13:08:03 +08001558 } else if (probeResult.isPartialConnectivity()) {
Chiachang Wang813ee472019-05-23 16:29:30 +08001559 mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_PARTIAL,
1560 null /* redirectUrl */);
Chiachang Wang813ee472019-05-23 16:29:30 +08001561 maybeDisableHttpsProbing(mAcceptPartialConnectivity);
lucaslind01ea622019-03-20 18:21:59 +08001562 if (mAcceptPartialConnectivity) {
lucaslind01ea622019-03-20 18:21:59 +08001563 transitionTo(mEvaluatingPrivateDnsState);
1564 } else {
1565 transitionTo(mWaitingForNextProbeState);
1566 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001567 } else {
1568 logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
Chiachang Wang813ee472019-05-23 16:29:30 +08001569 mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
1570 null /* redirectUrl */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001571 transitionTo(mWaitingForNextProbeState);
1572 }
1573 return HANDLED;
1574 case EVENT_DNS_NOTIFICATION:
lucaslinb0573962019-03-12 13:08:03 +08001575 case EVENT_ACCEPT_PARTIAL_CONNECTIVITY:
1576 // Leave the event to DefaultState.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001577 return NOT_HANDLED;
1578 default:
1579 // Wait for probe result and defer events to next state by default.
1580 deferMessage(message);
1581 return HANDLED;
1582 }
1583 }
1584
1585 @Override
1586 public void exit() {
1587 if (mThread.isAlive()) {
1588 mThread.interrupt();
1589 }
1590 mThread = null;
1591 }
1592 }
1593
1594 // Being in the WaitingForNextProbeState indicates that evaluating probes failed and state is
1595 // transited from ProbingState. This ensures that the state machine is only in ProbingState
1596 // while a probe is in progress, not while waiting to perform the next probe. That allows
1597 // ProbingState to defer most messages until the probe is complete, which keeps the code simple
1598 // and matches the pre-Q behaviour where probes were a blocking operation performed on the state
1599 // machine thread.
1600 private class WaitingForNextProbeState extends State {
1601 @Override
1602 public void enter() {
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09001603 // Send metrics for this evaluation attempt. Metrics collection (and its timers) will be
1604 // restarted when the next probe starts.
1605 maybeStopCollectionAndSendMetrics();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001606 scheduleNextProbe();
1607 }
1608
1609 private void scheduleNextProbe() {
1610 final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
1611 sendMessageDelayed(msg, mReevaluateDelayMs);
1612 mReevaluateDelayMs *= 2;
1613 if (mReevaluateDelayMs > MAX_REEVALUATE_DELAY_MS) {
1614 mReevaluateDelayMs = MAX_REEVALUATE_DELAY_MS;
1615 }
1616 }
1617
1618 @Override
1619 public boolean processMessage(Message message) {
1620 return NOT_HANDLED;
1621 }
1622 }
1623
Lucas Linfc5814c2020-05-05 08:15:19 +00001624 private final class EvaluatingBandwidthThread extends Thread {
1625 final int mThreadId;
1626
1627 EvaluatingBandwidthThread(int id) {
1628 mThreadId = id;
1629 }
1630
1631 @Override
1632 public void run() {
1633 HttpURLConnection urlConnection = null;
1634 try {
1635 final URL url = makeURL(mEvaluatingBandwidthUrl);
1636 urlConnection = makeProbeConnection(url, true /* followRedirects */);
1637 // In order to exclude the time of DNS lookup, send the delay message of timeout
1638 // here.
1639 sendMessageDelayed(CMD_BANDWIDTH_CHECK_TIMEOUT, mEvaluatingBandwidthTimeoutMs);
1640 readContentFromDownloadUrl(urlConnection);
1641 } catch (InterruptedIOException e) {
1642 // There is a timing issue that someone triggers the forcing reevaluation when
1643 // executing the getInputStream(). The InterruptedIOException is thrown by
1644 // Timeout#throwIfReached, it will reset the interrupt flag of Thread. So just
1645 // return and wait for the bandwidth reevaluation, otherwise the
1646 // CMD_BANDWIDTH_CHECK_COMPLETE will be sent.
1647 validationLog("The thread is interrupted when executing the getInputStream(),"
1648 + " return and wait for the bandwidth reevaluation");
1649 return;
1650 } catch (IOException e) {
1651 validationLog("Evaluating bandwidth failed: " + e + ", if the thread is not"
1652 + " interrupted, transition to validated state directly to make sure user"
1653 + " can use wifi normally.");
1654 } finally {
1655 if (urlConnection != null) {
1656 urlConnection.disconnect();
1657 }
1658 }
1659 // Don't send CMD_BANDWIDTH_CHECK_COMPLETE if the IO is interrupted or timeout.
1660 // Only send CMD_BANDWIDTH_CHECK_COMPLETE when the download is finished normally.
1661 // Add a serial number for CMD_BANDWIDTH_CHECK_COMPLETE to prevent handling the obsolete
1662 // CMD_BANDWIDTH_CHECK_COMPLETE.
1663 if (!isInterrupted()) sendMessage(CMD_BANDWIDTH_CHECK_COMPLETE, mThreadId);
1664 }
1665
1666 private void readContentFromDownloadUrl(@NonNull final HttpURLConnection conn)
1667 throws IOException {
1668 final byte[] buffer = new byte[1000];
1669 final InputStream is = conn.getInputStream();
1670 while (!isInterrupted() && is.read(buffer) > 0) { /* read again */ }
1671 }
1672 }
1673
1674 private class EvaluatingBandwidthState extends State {
1675 private EvaluatingBandwidthThread mEvaluatingBandwidthThread;
1676 private int mRetryBandwidthDelayMs;
1677 private int mCurrentThreadId;
1678
1679 @Override
1680 public void enter() {
1681 mRetryBandwidthDelayMs = getResIntConfig(mContext,
1682 R.integer.config_evaluating_bandwidth_min_retry_timer_ms,
1683 INITIAL_REEVALUATE_DELAY_MS);
1684 sendMessage(CMD_EVALUATE_BANDWIDTH);
1685 }
1686
1687 @Override
1688 public boolean processMessage(Message msg) {
1689 switch (msg.what) {
1690 case CMD_EVALUATE_BANDWIDTH:
1691 mCurrentThreadId = mNextEvaluatingBandwidthThreadId.getAndIncrement();
1692 mEvaluatingBandwidthThread = new EvaluatingBandwidthThread(mCurrentThreadId);
1693 mEvaluatingBandwidthThread.start();
1694 break;
1695 case CMD_BANDWIDTH_CHECK_COMPLETE:
1696 // Only handle the CMD_BANDWIDTH_CHECK_COMPLETE which is sent by the newest
1697 // EvaluatingBandwidthThread.
1698 if (mCurrentThreadId == msg.arg1) {
1699 mIsBandwidthCheckPassedOrIgnored = true;
1700 transitionTo(mValidatedState);
1701 }
1702 break;
1703 case CMD_BANDWIDTH_CHECK_TIMEOUT:
1704 validationLog("Evaluating bandwidth timeout!");
1705 mEvaluatingBandwidthThread.interrupt();
1706 scheduleReevaluatingBandwidth();
1707 break;
1708 default:
1709 return NOT_HANDLED;
1710 }
1711 return HANDLED;
1712 }
1713
1714 private void scheduleReevaluatingBandwidth() {
1715 sendMessageDelayed(obtainMessage(CMD_EVALUATE_BANDWIDTH), mRetryBandwidthDelayMs);
1716 mRetryBandwidthDelayMs *= 2;
1717 if (mRetryBandwidthDelayMs > mMaxRetryTimerMs) {
1718 mRetryBandwidthDelayMs = mMaxRetryTimerMs;
1719 }
1720 }
1721
1722 @Override
1723 public void exit() {
1724 mEvaluatingBandwidthThread.interrupt();
1725 removeMessages(CMD_EVALUATE_BANDWIDTH);
1726 removeMessages(CMD_BANDWIDTH_CHECK_TIMEOUT);
1727 }
1728 }
1729
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001730 // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
1731 // most one per address family. This ensures we only wait up to 20 seconds for TCP connections
1732 // to complete, regardless of how many IP addresses a host has.
1733 private static class OneAddressPerFamilyNetwork extends Network {
1734 OneAddressPerFamilyNetwork(Network network) {
1735 // Always bypass Private DNS.
1736 super(network.getPrivateDnsBypassingCopy());
1737 }
1738
1739 @Override
1740 public InetAddress[] getAllByName(String host) throws UnknownHostException {
1741 final List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
1742
1743 // Ensure the address family of the first address is tried first.
1744 LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
1745 addressByFamily.put(addrs.get(0).getClass(), addrs.get(0));
1746 Collections.shuffle(addrs);
1747
1748 for (InetAddress addr : addrs) {
1749 addressByFamily.put(addr.getClass(), addr);
1750 }
1751
1752 return addressByFamily.values().toArray(new InetAddress[addressByFamily.size()]);
1753 }
1754 }
1755
Lucas Linfc5814c2020-05-05 08:15:19 +00001756 @VisibleForTesting
1757 boolean onlyWifiTransport() {
1758 int[] transportTypes = mNetworkCapabilities.getTransportTypes();
1759 return transportTypes.length == 1
1760 && transportTypes[0] == NetworkCapabilities.TRANSPORT_WIFI;
1761 }
1762
1763 @VisibleForTesting
1764 boolean needEvaluatingBandwidth() {
1765 if (mIsBandwidthCheckPassedOrIgnored
1766 || TextUtils.isEmpty(mEvaluatingBandwidthUrl)
1767 || !mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
1768 || !onlyWifiTransport()) {
1769 return false;
1770 }
1771
1772 return true;
1773 }
1774
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001775 private boolean getIsCaptivePortalCheckEnabled() {
Chiachang Wang969eb752019-04-25 09:47:27 +08001776 String symbol = CAPTIVE_PORTAL_MODE;
1777 int defaultValue = CAPTIVE_PORTAL_MODE_PROMPT;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001778 int mode = mDependencies.getSetting(mContext, symbol, defaultValue);
Chiachang Wang969eb752019-04-25 09:47:27 +08001779 return mode != CAPTIVE_PORTAL_MODE_IGNORE;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001780 }
1781
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00001782 private boolean getIsPrivateIpNoInternetEnabled() {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00001783 return mDependencies.isFeatureEnabled(mContext, DNS_PROBE_PRIVATE_IP_NO_INTERNET_VERSION)
1784 || mContext.getResources().getBoolean(
1785 R.bool.config_force_dns_probe_private_ip_no_internet);
1786 }
1787
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001788 private boolean getUseHttpsValidation() {
Chiachang Wang79a6da32019-04-17 17:00:54 +08001789 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
1790 CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001791 }
1792
lucaslin9b4dfab2019-12-17 23:06:12 +08001793 @Nullable
1794 private String getMccFromCellInfo(final CellInfo cell) {
1795 if (cell instanceof CellInfoGsm) {
1796 return ((CellInfoGsm) cell).getCellIdentity().getMccString();
1797 } else if (cell instanceof CellInfoLte) {
1798 return ((CellInfoLte) cell).getCellIdentity().getMccString();
1799 } else if (cell instanceof CellInfoWcdma) {
1800 return ((CellInfoWcdma) cell).getCellIdentity().getMccString();
1801 } else if (cell instanceof CellInfoTdscdma) {
1802 return ((CellInfoTdscdma) cell).getCellIdentity().getMccString();
1803 } else if (cell instanceof CellInfoNr) {
1804 return ((CellIdentityNr) ((CellInfoNr) cell).getCellIdentity()).getMccString();
1805 } else {
1806 return null;
1807 }
1808 }
1809
1810 /**
1811 * Return location mcc.
1812 */
1813 @VisibleForTesting
1814 @Nullable
1815 protected String getLocationMcc() {
1816 // Adding this check is because the new permission won't be granted by mainline update,
1817 // the new permission only be granted by OTA for current design. Tracking: b/145774617.
1818 if (mContext.checkPermission(android.Manifest.permission.ACCESS_FINE_LOCATION,
1819 Process.myPid(), Process.myUid())
1820 == PackageManager.PERMISSION_DENIED) {
1821 log("getLocationMcc : NetworkStack does not hold ACCESS_FINE_LOCATION");
1822 return null;
1823 }
1824 try {
1825 final List<CellInfo> cells = mTelephonyManager.getAllCellInfo();
Lucas Lin10039a42020-06-02 11:01:41 +00001826 if (cells == null) return null;
lucaslin9b4dfab2019-12-17 23:06:12 +08001827 final Map<String, Integer> countryCodeMap = new HashMap<>();
1828 int maxCount = 0;
1829 for (final CellInfo cell : cells) {
1830 final String mcc = getMccFromCellInfo(cell);
1831 if (mcc != null) {
1832 final int count = countryCodeMap.getOrDefault(mcc, 0) + 1;
1833 countryCodeMap.put(mcc, count);
1834 }
1835 }
1836 // Return the MCC which occurs most.
1837 if (countryCodeMap.size() <= 0) return null;
1838 return Collections.max(countryCodeMap.entrySet(),
1839 (e1, e2) -> e1.getValue().compareTo(e2.getValue())).getKey();
1840 } catch (SecurityException e) {
1841 log("Permission is not granted:" + e);
1842 return null;
1843 }
1844 }
1845
Lucas Lin044be5f2020-04-16 02:17:00 +00001846 /**
1847 * Return a matched MccMncOverrideInfo if carrier id and sim mccmnc are matching a record in
1848 * sCarrierIdToMccMnc.
1849 */
lucaslin9b4dfab2019-12-17 23:06:12 +08001850 @VisibleForTesting
Lucas Lin044be5f2020-04-16 02:17:00 +00001851 @Nullable
1852 MccMncOverrideInfo getMccMncOverrideInfo() {
1853 final int carrierId = mTelephonyManager.getSimCarrierId();
1854 return sCarrierIdToMccMnc.get(carrierId);
1855 }
1856
1857 private Context getContextByMccMnc(final int mcc, final int mnc) {
1858 final Configuration config = mContext.getResources().getConfiguration();
1859 if (mcc != UNSET_MCC_OR_MNC) config.mcc = mcc;
1860 if (mnc != UNSET_MCC_OR_MNC) config.mnc = mnc;
1861 return mContext.createConfigurationContext(config);
1862 }
1863
1864 @VisibleForTesting
1865 protected Context getCustomizedContextOrDefault() {
1866 // Return customized context if carrier id can match a record in sCarrierIdToMccMnc.
1867 final MccMncOverrideInfo overrideInfo = getMccMncOverrideInfo();
1868 if (overrideInfo != null) {
1869 return getContextByMccMnc(overrideInfo.mcc, overrideInfo.mnc);
1870 }
1871
1872 // Use neighbor mcc feature only works when the config_no_sim_card_uses_neighbor_mcc is
1873 // true and there is no sim card inserted.
lucaslin9b4dfab2019-12-17 23:06:12 +08001874 final boolean useNeighborResource =
lucaslinc3d9f712020-04-10 03:28:12 +00001875 getResBooleanConfig(mContext, R.bool.config_no_sim_card_uses_neighbor_mcc, false);
lucaslin9b4dfab2019-12-17 23:06:12 +08001876 if (!useNeighborResource
1877 || TelephonyManager.SIM_STATE_READY == mTelephonyManager.getSimState()) {
1878 return mContext;
1879 }
Lucas Lin044be5f2020-04-16 02:17:00 +00001880
lucaslin9b4dfab2019-12-17 23:06:12 +08001881 final String mcc = getLocationMcc();
1882 if (TextUtils.isEmpty(mcc)) {
1883 return mContext;
1884 }
Lucas Lin044be5f2020-04-16 02:17:00 +00001885
1886 return getContextByMccMnc(Integer.parseInt(mcc), UNSET_MCC_OR_MNC);
lucaslin9b4dfab2019-12-17 23:06:12 +08001887 }
1888
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +00001889 @Nullable
1890 private String getTestUrl(@NonNull String key) {
1891 final String strExpiration = mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
1892 TEST_URL_EXPIRATION_TIME, null);
1893 if (strExpiration == null) return null;
1894
1895 final long expTime;
1896 try {
1897 expTime = Long.parseUnsignedLong(strExpiration);
1898 } catch (NumberFormatException e) {
1899 loge("Invalid test URL expiration time format", e);
1900 return null;
1901 }
1902
1903 final long now = System.currentTimeMillis();
1904 if (expTime < now || (expTime - now) > TEST_URL_EXPIRATION_MS) return null;
1905
1906 return mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
1907 key, null /* defaultValue */);
1908 }
1909
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09001910 private String getCaptivePortalServerHttpsUrl() {
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +00001911 final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
1912 if (isValidTestUrl(testUrl)) return testUrl;
Lucas Lin044be5f2020-04-16 02:17:00 +00001913 final Context targetContext = getCustomizedContextOrDefault();
Chiachang Wang44ee27b2020-04-10 09:02:45 +08001914 return getSettingFromResource(targetContext,
1915 R.string.config_captive_portal_https_url, CAPTIVE_PORTAL_HTTPS_URL,
1916 targetContext.getResources().getString(R.string.default_captive_portal_https_url));
Niklas Lindgren0c904882018-12-07 11:08:04 +01001917 }
1918
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +00001919 private static boolean isValidTestUrl(@Nullable String url) {
1920 if (TextUtils.isEmpty(url)) return false;
1921
1922 try {
1923 // Only accept test URLs on localhost
1924 return Uri.parse(url).getHost().equals("localhost");
1925 } catch (Throwable e) {
1926 Log.wtf(TAG, "Error parsing test URL", e);
1927 return false;
1928 }
1929 }
1930
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09001931 private int getDnsProbeTimeout() {
1932 return getIntSetting(mContext, R.integer.config_captive_portal_dns_probe_timeout,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08001933 CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT, DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT);
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09001934 }
1935
1936 /**
1937 * Gets an integer setting from resources or device config
1938 *
Chiachang Wang44ee27b2020-04-10 09:02:45 +08001939 * configResource is used if set, followed by device config if set, followed by defaultValue.
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09001940 * If none of these are set then an exception is thrown.
1941 *
1942 * TODO: move to a common location such as a ConfigUtils class.
1943 * TODO(b/130324939): test that the resources can be overlayed by an RRO package.
1944 */
1945 @VisibleForTesting
1946 int getIntSetting(@NonNull final Context context, @StringRes int configResource,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08001947 @NonNull String symbol, int defaultValue) {
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09001948 final Resources res = context.getResources();
1949 try {
1950 return res.getInteger(configResource);
1951 } catch (Resources.NotFoundException e) {
1952 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08001953 symbol, defaultValue);
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09001954 }
1955 }
1956
lucaslin9b4dfab2019-12-17 23:06:12 +08001957 @VisibleForTesting
lucaslinc3d9f712020-04-10 03:28:12 +00001958 boolean getResBooleanConfig(@NonNull final Context context,
1959 @BoolRes int configResource, final boolean defaultValue) {
lucaslin9b4dfab2019-12-17 23:06:12 +08001960 final Resources res = context.getResources();
1961 try {
1962 return res.getBoolean(configResource);
1963 } catch (Resources.NotFoundException e) {
lucaslinc3d9f712020-04-10 03:28:12 +00001964 return defaultValue;
1965 }
1966 }
1967
1968 /**
1969 * Gets integer config from resources.
1970 */
1971 @VisibleForTesting
1972 int getResIntConfig(@NonNull final Context context,
1973 @IntegerRes final int configResource, final int defaultValue) {
1974 final Resources res = context.getResources();
1975 try {
1976 return res.getInteger(configResource);
1977 } catch (Resources.NotFoundException e) {
1978 return defaultValue;
1979 }
1980 }
1981
1982 /**
1983 * Gets string config from resources.
1984 */
1985 @VisibleForTesting
1986 String getResStringConfig(@NonNull final Context context,
1987 @StringRes final int configResource, @Nullable final String defaultValue) {
1988 final Resources res = context.getResources();
1989 try {
1990 return res.getString(configResource);
1991 } catch (Resources.NotFoundException e) {
1992 return defaultValue;
lucaslin9b4dfab2019-12-17 23:06:12 +08001993 }
1994 }
1995
Niklas Lindgren0c904882018-12-07 11:08:04 +01001996 /**
1997 * Get the captive portal server HTTP URL that is configured on the device.
1998 *
1999 * NetworkMonitor does not use {@link ConnectivityManager#getCaptivePortalServerUrl()} as
2000 * it has its own updatable strategies to detect captive portals. The framework only advises
2001 * on one URL that can be used, while NetworkMonitor may implement more complex logic.
2002 */
2003 public String getCaptivePortalServerHttpUrl() {
Remi NGUYEN VAN259e6f02020-04-28 06:16:55 +00002004 final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
2005 if (isValidTestUrl(testUrl)) return testUrl;
Lucas Lin044be5f2020-04-16 02:17:00 +00002006 final Context targetContext = getCustomizedContextOrDefault();
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002007 return getSettingFromResource(targetContext,
2008 R.string.config_captive_portal_http_url, CAPTIVE_PORTAL_HTTP_URL,
2009 targetContext.getResources().getString(R.string.default_captive_portal_http_url));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002010 }
2011
2012 private int getConsecutiveDnsTimeoutThreshold() {
Chiachang Wang9a87f802019-04-08 19:06:21 +08002013 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
2014 CONFIG_DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002015 DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD);
2016 }
2017
2018 private int getDataStallMinEvaluateTime() {
Chiachang Wang9a87f802019-04-08 19:06:21 +08002019 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
2020 CONFIG_DATA_STALL_MIN_EVALUATE_INTERVAL,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002021 DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS);
2022 }
2023
2024 private int getDataStallValidDnsTimeThreshold() {
Chiachang Wang9a87f802019-04-08 19:06:21 +08002025 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
2026 CONFIG_DATA_STALL_VALID_DNS_TIME_THRESHOLD,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002027 DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS);
2028 }
2029
Chiachang Wang0b34ae62020-05-21 07:03:23 +00002030 @VisibleForTesting
2031 int getDataStallEvaluationType() {
Chiachang Wang9a87f802019-04-08 19:06:21 +08002032 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
2033 CONFIG_DATA_STALL_EVALUATION_TYPE,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002034 DEFAULT_DATA_STALL_EVALUATION_TYPES);
2035 }
2036
Chiachang Wanga5716bf2019-11-20 16:13:07 +08002037 private int getTcpPollingInterval() {
2038 return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
2039 CONFIG_DATA_STALL_TCP_POLLING_INTERVAL,
2040 DEFAULT_TCP_POLLING_INTERVAL_MS);
2041 }
2042
Chiachang Wang91f0b5b2020-03-24 11:23:43 +00002043 @VisibleForTesting
2044 URL[] makeCaptivePortalFallbackUrls() {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002045 try {
Chiachang Wang969eb752019-04-25 09:47:27 +08002046 final String firstUrl = mDependencies.getSetting(mContext, CAPTIVE_PORTAL_FALLBACK_URL,
2047 null);
Chiachang Wanga64bb702020-03-31 07:50:59 +00002048 final URL[] settingProviderUrls =
2049 combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_FALLBACK_URLS);
Chiachang Wang65ba4e72020-03-23 11:52:43 +00002050 return getProbeUrlArrayConfig(settingProviderUrls,
2051 R.array.config_captive_portal_fallback_urls,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002052 R.array.default_captive_portal_fallback_urls,
2053 this::makeURL);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002054 } catch (Exception e) {
2055 // Don't let a misconfiguration bootloop the system.
2056 Log.e(TAG, "Error parsing configured fallback URLs", e);
2057 return new URL[0];
2058 }
2059 }
2060
2061 private CaptivePortalProbeSpec[] makeCaptivePortalFallbackProbeSpecs() {
2062 try {
Chiachang Wang79a6da32019-04-17 17:00:54 +08002063 final String settingsValue = mDependencies.getDeviceConfigProperty(
2064 NAMESPACE_CONNECTIVITY, CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS, null);
2065
Niklas Lindgren0c904882018-12-07 11:08:04 +01002066 final CaptivePortalProbeSpec[] emptySpecs = new CaptivePortalProbeSpec[0];
2067 final CaptivePortalProbeSpec[] providerValue = TextUtils.isEmpty(settingsValue)
2068 ? emptySpecs
2069 : parseCaptivePortalProbeSpecs(settingsValue).toArray(emptySpecs);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002070
Chiachang Wang65ba4e72020-03-23 11:52:43 +00002071 return getProbeUrlArrayConfig(providerValue,
2072 R.array.config_captive_portal_fallback_probe_specs,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002073 DEFAULT_CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS,
Niklas Lindgren0c904882018-12-07 11:08:04 +01002074 CaptivePortalProbeSpec::parseSpecOrNull);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002075 } catch (Exception e) {
2076 // Don't let a misconfiguration bootloop the system.
2077 Log.e(TAG, "Error parsing configured fallback probe specs", e);
2078 return null;
2079 }
2080 }
2081
Chiachang Wanga64bb702020-03-31 07:50:59 +00002082 private URL[] makeCaptivePortalHttpsUrls() {
2083 final String firstUrl = getCaptivePortalServerHttpsUrl();
2084 try {
2085 final URL[] settingProviderUrls =
2086 combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTPS_URLS);
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002087 // firstUrl will at least be default configuration, so default value in
2088 // getProbeUrlArrayConfig is actually never used.
Chiachang Wanga64bb702020-03-31 07:50:59 +00002089 return getProbeUrlArrayConfig(settingProviderUrls,
2090 R.array.config_captive_portal_https_urls,
2091 DEFAULT_CAPTIVE_PORTAL_HTTPS_URLS, this::makeURL);
2092 } catch (Exception e) {
2093 // Don't let a misconfiguration bootloop the system.
2094 Log.e(TAG, "Error parsing configured https URLs", e);
2095 // Ensure URL aligned with legacy configuration.
2096 return new URL[]{makeURL(firstUrl)};
2097 }
2098 }
2099
2100 private URL[] makeCaptivePortalHttpUrls() {
2101 final String firstUrl = getCaptivePortalServerHttpUrl();
2102 try {
2103 final URL[] settingProviderUrls =
2104 combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTP_URLS);
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002105 // firstUrl will at least be default configuration, so default value in
2106 // getProbeUrlArrayConfig is actually never used.
Chiachang Wanga64bb702020-03-31 07:50:59 +00002107 return getProbeUrlArrayConfig(settingProviderUrls,
2108 R.array.config_captive_portal_http_urls,
2109 DEFAULT_CAPTIVE_PORTAL_HTTP_URLS, this::makeURL);
2110 } catch (Exception e) {
2111 // Don't let a misconfiguration bootloop the system.
2112 Log.e(TAG, "Error parsing configured http URLs", e);
2113 // Ensure URL aligned with legacy configuration.
2114 return new URL[]{makeURL(firstUrl)};
2115 }
2116 }
2117
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002118 private URL[] combineCaptivePortalUrls(final String firstUrl, final String propertyName) {
Chiachang Wanga64bb702020-03-31 07:50:59 +00002119 if (TextUtils.isEmpty(firstUrl)) return new URL[0];
2120
2121 final String otherUrls = mDependencies.getDeviceConfigProperty(
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002122 NAMESPACE_CONNECTIVITY, propertyName, "");
Chiachang Wanga64bb702020-03-31 07:50:59 +00002123 // otherUrls may be empty, but .split() ignores trailing empty strings
2124 final String separator = ",";
2125 final String[] urls = (firstUrl + separator + otherUrls).split(separator);
2126 return convertStrings(urls, this::makeURL, new URL[0]);
2127 }
2128
Niklas Lindgren0c904882018-12-07 11:08:04 +01002129 /**
2130 * Read a setting from a resource or the settings provider.
2131 *
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002132 * <p>The configuration resource is prioritized, then the provider value.
Niklas Lindgren0c904882018-12-07 11:08:04 +01002133 * @param context The context
2134 * @param configResource The resource id for the configuration parameter
Niklas Lindgren0c904882018-12-07 11:08:04 +01002135 * @param symbol The symbol in the settings provider
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002136 * @param defaultValue The default value
Niklas Lindgren0c904882018-12-07 11:08:04 +01002137 * @return The best available value
2138 */
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002139 @Nullable
Niklas Lindgren0c904882018-12-07 11:08:04 +01002140 private String getSettingFromResource(@NonNull final Context context,
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002141 @StringRes int configResource, @NonNull String symbol, @NonNull String defaultValue) {
Niklas Lindgren0c904882018-12-07 11:08:04 +01002142 final Resources res = context.getResources();
2143 String setting = res.getString(configResource);
2144
2145 if (!TextUtils.isEmpty(setting)) return setting;
2146
2147 setting = mDependencies.getSetting(context, symbol, null);
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002148
Niklas Lindgren0c904882018-12-07 11:08:04 +01002149 if (!TextUtils.isEmpty(setting)) return setting;
2150
Chiachang Wang44ee27b2020-04-10 09:02:45 +08002151 return defaultValue;
Niklas Lindgren0c904882018-12-07 11:08:04 +01002152 }
2153
2154 /**
2155 * Get an array configuration from resources or the settings provider.
2156 *
2157 * <p>The configuration resource is prioritized, then the provider values, then the default
2158 * resource values.
2159 * @param providerValue Values obtained from the setting provider.
2160 * @param configResId ID of the configuration resource.
2161 * @param defaultResId ID of the default resource.
2162 * @param resourceConverter Converter from the resource strings to stored setting class. Null
2163 * return values are ignored.
2164 */
Chiachang Wang65ba4e72020-03-23 11:52:43 +00002165 private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
Niklas Lindgren0c904882018-12-07 11:08:04 +01002166 @ArrayRes int defaultResId, @NonNull Function<String, T> resourceConverter) {
Lucas Lin044be5f2020-04-16 02:17:00 +00002167 final Resources res = getCustomizedContextOrDefault().getResources();
Chiachang Wanga64bb702020-03-31 07:50:59 +00002168 return getProbeUrlArrayConfig(providerValue, configResId, res.getStringArray(defaultResId),
2169 resourceConverter);
2170 }
2171
2172 /**
2173 * Get an array configuration from resources or the settings provider.
2174 *
2175 * <p>The configuration resource is prioritized, then the provider values, then the default
2176 * resource values.
2177 * @param providerValue Values obtained from the setting provider.
2178 * @param configResId ID of the configuration resource.
2179 * @param defaultConfig Values of default configuration.
2180 * @param resourceConverter Converter from the resource strings to stored setting class. Null
2181 * return values are ignored.
2182 */
2183 private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
2184 String[] defaultConfig, @NonNull Function<String, T> resourceConverter) {
Lucas Lin044be5f2020-04-16 02:17:00 +00002185 final Resources res = getCustomizedContextOrDefault().getResources();
Niklas Lindgren0c904882018-12-07 11:08:04 +01002186 String[] configValue = res.getStringArray(configResId);
2187
2188 if (configValue.length == 0) {
2189 if (providerValue.length > 0) {
2190 return providerValue;
2191 }
2192
Chiachang Wanga64bb702020-03-31 07:50:59 +00002193 configValue = defaultConfig;
Niklas Lindgren0c904882018-12-07 11:08:04 +01002194 }
2195
2196 return convertStrings(configValue, resourceConverter, Arrays.copyOf(providerValue, 0));
2197 }
2198
2199 /**
2200 * Convert a String array to an array of some other type using the specified converter.
2201 *
2202 * <p>Any null value, or value for which the converter throws a {@link RuntimeException}, will
2203 * not be added to the output array, so the output array may be smaller than the input.
2204 */
2205 private <T> T[] convertStrings(
2206 @NonNull String[] strings, Function<String, T> converter, T[] emptyArray) {
2207 final ArrayList<T> convertedValues = new ArrayList<>(strings.length);
2208 for (String configString : strings) {
2209 T convertedValue = null;
2210 try {
2211 convertedValue = converter.apply(configString);
2212 } catch (Exception e) {
2213 Log.e(TAG, "Error parsing configuration", e);
2214 // Fall through
2215 }
2216 if (convertedValue != null) {
2217 convertedValues.add(convertedValue);
2218 }
2219 }
2220 return convertedValues.toArray(emptyArray);
2221 }
2222
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002223 private String getCaptivePortalUserAgent() {
Chiachang Wang79a6da32019-04-17 17:00:54 +08002224 return mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
2225 CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002226 }
2227
2228 private URL nextFallbackUrl() {
2229 if (mCaptivePortalFallbackUrls.length == 0) {
2230 return null;
2231 }
2232 int idx = Math.abs(mNextFallbackUrlIndex) % mCaptivePortalFallbackUrls.length;
2233 mNextFallbackUrlIndex += mRandom.nextInt(); // randomly change url without memory.
2234 return mCaptivePortalFallbackUrls[idx];
2235 }
2236
2237 private CaptivePortalProbeSpec nextFallbackSpec() {
Remi NGUYEN VANabeaaf72019-01-20 13:48:19 +09002238 if (isEmpty(mCaptivePortalFallbackSpecs)) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002239 return null;
2240 }
2241 // Randomly change spec without memory. Also randomize the first attempt.
2242 final int idx = Math.abs(mRandom.nextInt()) % mCaptivePortalFallbackSpecs.length;
2243 return mCaptivePortalFallbackSpecs[idx];
2244 }
2245
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002246 /**
2247 * Parameters that can be accessed by the evaluation thread in a thread-safe way.
2248 *
2249 * Parameters such as LinkProperties and NetworkCapabilities cannot be accessed by the
2250 * evaluation thread directly, as they are managed in the state machine thread and not
2251 * synchronized. This class provides a copy of the required data that is not modified and can be
2252 * used safely by the evaluation thread.
2253 */
2254 private static class EvaluationThreadDeps {
2255 // TODO: add parameters that are accessed in a non-thread-safe way from the evaluation
2256 // thread (read from LinkProperties, NetworkCapabilities, useHttps, validationStage)
2257 private final boolean mIsTestNetwork;
2258
2259 EvaluationThreadDeps(NetworkCapabilities nc) {
2260 this.mIsTestNetwork = nc.hasTransport(TRANSPORT_TEST);
2261 }
2262 }
2263
2264 private CaptivePortalProbeResult isCaptivePortal(EvaluationThreadDeps deps) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002265 if (!mIsCaptivePortalCheckEnabled) {
2266 validationLog("Validation disabled.");
Chiachang Wang50865812020-04-14 16:26:24 +00002267 return CaptivePortalProbeResult.success(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002268 }
2269
2270 URL pacUrl = null;
Chiachang Wanga64bb702020-03-31 07:50:59 +00002271 final URL[] httpsUrls = mCaptivePortalHttpsUrls;
2272 final URL[] httpUrls = mCaptivePortalHttpUrls;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002273
2274 // On networks with a PAC instead of fetching a URL that should result in a 204
2275 // response, we instead simply fetch the PAC script. This is done for a few reasons:
2276 // 1. At present our PAC code does not yet handle multiple PACs on multiple networks
2277 // until something like https://android-review.googlesource.com/#/c/115180/ lands.
2278 // Network.openConnection() will ignore network-specific PACs and instead fetch
2279 // using NO_PROXY. If a PAC is in place, the only fetch we know will succeed with
2280 // NO_PROXY is the fetch of the PAC itself.
2281 // 2. To proxy the generate_204 fetch through a PAC would require a number of things
2282 // happen before the fetch can commence, namely:
2283 // a) the PAC script be fetched
2284 // b) a PAC script resolver service be fired up and resolve the captive portal
2285 // server.
2286 // Network validation could be delayed until these prerequisities are satisifed or
2287 // could simply be left to race them. Neither is an optimal solution.
2288 // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
2289 // fact block fetching of the generate_204 URL which would lead to false negative
2290 // results for network validation.
2291 final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
2292 if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
2293 pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
2294 if (pacUrl == null) {
Chiachang Wang50865812020-04-14 16:26:24 +00002295 return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002296 }
2297 }
2298
Chiachang Wanga64bb702020-03-31 07:50:59 +00002299 if ((pacUrl == null) && (httpUrls.length == 0 || httpsUrls.length == 0
2300 || httpUrls[0] == null || httpsUrls[0] == null)) {
Chiachang Wang50865812020-04-14 16:26:24 +00002301 return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002302 }
2303
2304 long startTime = SystemClock.elapsedRealtime();
2305
2306 final CaptivePortalProbeResult result;
2307 if (pacUrl != null) {
2308 result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
Chiachang Wang813ee472019-05-23 16:29:30 +08002309 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
Chiachang Wang3278b382020-04-16 13:04:02 +00002310 } else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
2311 // Probe results are reported inside sendHttpAndHttpsParallelWithFallbackProbes.
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002312 result = sendHttpAndHttpsParallelWithFallbackProbes(deps, proxyInfo,
2313 httpsUrls[0], httpUrls[0]);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002314 } else if (mUseHttps) {
Chiachang Wang3278b382020-04-16 13:04:02 +00002315 // Support result aggregation from multiple Urls.
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002316 result = sendMultiParallelHttpAndHttpsProbes(deps, proxyInfo, httpsUrls, httpUrls);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002317 } else {
Chiachang Wanga64bb702020-03-31 07:50:59 +00002318 result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
Chiachang Wang813ee472019-05-23 16:29:30 +08002319 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002320 }
2321
2322 long endTime = SystemClock.elapsedRealtime();
2323
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002324 log("isCaptivePortal: isSuccessful()=" + result.isSuccessful()
2325 + " isPortal()=" + result.isPortal()
2326 + " RedirectUrl=" + result.redirectUrl
Chiachang Wang813ee472019-05-23 16:29:30 +08002327 + " isPartialConnectivity()=" + result.isPartialConnectivity()
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002328 + " Time=" + (endTime - startTime) + "ms");
2329
2330 return result;
2331 }
2332
2333 /**
2334 * Do a DNS resolution and URL fetch on a known web server to see if we get the data we expect.
2335 * @return a CaptivePortalProbeResult inferred from the HTTP response.
2336 */
2337 private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
2338 // Pre-resolve the captive portal server host so we can log it.
2339 // Only do this if HttpURLConnection is about to, to avoid any potentially
2340 // unnecessary resolution.
2341 final String host = (proxy != null) ? proxy.getHost() : url.getHost();
Chiachang Wang813ee472019-05-23 16:29:30 +08002342 // This method cannot safely report probe results because it might not be running on the
2343 // state machine thread. Reporting results here would cause races and potentially send
2344 // information to callers that does not make sense because the state machine has already
2345 // changed state.
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002346 final InetAddress[] resolvedAddr = sendDnsProbe(host);
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00002347 // The private IP logic only applies to captive portal detection (the HTTP probe), not
2348 // network validation (the HTTPS probe, which would likely fail anyway) or the PAC probe.
2349 if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002350 && (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09002351 recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
Frank Lie04f8b72020-06-22 12:45:27 +00002352 0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002353 return CaptivePortalProbeResult.PRIVATE_IP;
2354 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002355 return sendHttpProbe(url, probeType, null);
2356 }
2357
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09002358 /** Do a DNS lookup for the given server, or throw UnknownHostException after timeoutMs */
2359 @VisibleForTesting
2360 protected InetAddress[] sendDnsProbeWithTimeout(String host, int timeoutMs)
2361 throws UnknownHostException {
Chiachang Wang1c67f4e2019-05-09 21:28:47 +08002362 return DnsUtils.getAllByName(mDependencies.getDnsResolver(), mCleartextDnsNetwork, host,
Chiachang Wangddb7da62019-06-03 15:50:53 +08002363 TYPE_ADDRCONFIG, FLAG_EMPTY, timeoutMs,
2364 str -> validationLog(ValidationProbeEvent.PROBE_DNS, host, str));
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09002365 }
2366
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002367 /** Do a DNS resolution of the given server. */
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002368 private InetAddress[] sendDnsProbe(String host) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002369 if (TextUtils.isEmpty(host)) {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002370 return null;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002371 }
2372
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002373 final Stopwatch watch = new Stopwatch().start();
2374 int result;
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002375 InetAddress[] addresses;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002376 try {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002377 addresses = sendDnsProbeWithTimeout(host, getDnsProbeTimeout());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002378 result = ValidationProbeEvent.DNS_SUCCESS;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002379 } catch (UnknownHostException e) {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002380 addresses = null;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002381 result = ValidationProbeEvent.DNS_FAILURE;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002382 }
2383 final long latency = watch.stop();
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09002384 recordProbeEventMetrics(ProbeType.PT_DNS, latency,
Frank Lie04f8b72020-06-22 12:45:27 +00002385 (result == ValidationProbeEvent.DNS_SUCCESS) ? ProbeResult.PR_SUCCESS :
2386 ProbeResult.PR_FAILURE, null /* capportData */);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002387 logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result);
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002388 return addresses;
2389 }
2390
2391 /**
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00002392 * Check if any of the provided IP addresses include a private IP.
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002393 * @return true if an IP address is private.
2394 */
2395 private static boolean hasPrivateIpAddress(@Nullable InetAddress[] addresses) {
2396 if (addresses == null) {
2397 return false;
2398 }
2399 for (InetAddress address : addresses) {
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00002400 if (address.isLinkLocalAddress() || address.isSiteLocalAddress()
2401 || isIPv6ULA(address)) {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002402 return true;
2403 }
2404 }
2405 return false;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002406 }
2407
2408 /**
2409 * Do a URL fetch on a known web server to see if we get the data we expect.
2410 * @return a CaptivePortalProbeResult inferred from the HTTP response.
2411 */
2412 @VisibleForTesting
2413 protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
2414 @Nullable CaptivePortalProbeSpec probeSpec) {
2415 HttpURLConnection urlConnection = null;
2416 int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
2417 String redirectUrl = null;
2418 final Stopwatch probeTimer = new Stopwatch().start();
Chalard Jeanac5b8992019-04-09 11:16:56 +09002419 final int oldTag = TrafficStats.getAndSetThreadStatsTag(
2420 TrafficStatsConstants.TAG_SYSTEM_PROBE);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002421 try {
Remi NGUYEN VAN2d909a72019-12-24 18:15:52 +09002422 // Follow redirects for PAC probes as such probes verify connectivity by fetching the
2423 // PAC proxy file, which may be configured behind a redirect.
2424 final boolean followRedirect = probeType == ValidationProbeEvent.PROBE_PAC;
2425 urlConnection = makeProbeConnection(url, followRedirect);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002426 // cannot read request header after connection
2427 String requestHeader = urlConnection.getRequestProperties().toString();
2428
2429 // Time how long it takes to get a response to our request
2430 long requestTimestamp = SystemClock.elapsedRealtime();
2431
2432 httpResponseCode = urlConnection.getResponseCode();
2433 redirectUrl = urlConnection.getHeaderField("location");
2434
2435 // Time how long it takes to get a response to our request
2436 long responseTimestamp = SystemClock.elapsedRealtime();
2437
2438 validationLog(probeType, url, "time=" + (responseTimestamp - requestTimestamp) + "ms"
2439 + " ret=" + httpResponseCode
2440 + " request=" + requestHeader
2441 + " headers=" + urlConnection.getHeaderFields());
2442 // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
2443 // portal. The only example of this seen so far was a captive portal. For
2444 // the time being go with prior behavior of assuming it's not a captive
2445 // portal. If it is considered a captive portal, a different sign-in URL
2446 // is needed (i.e. can't browse a 204). This could be the result of an HTTP
2447 // proxy server.
2448 if (httpResponseCode == 200) {
Sehee Park43427c02018-11-16 17:39:34 +09002449 long contentLength = urlConnection.getContentLengthLong();
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002450 if (probeType == ValidationProbeEvent.PROBE_PAC) {
2451 validationLog(
2452 probeType, url, "PAC fetch 200 response interpreted as 204 response.");
2453 httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
Sehee Park43427c02018-11-16 17:39:34 +09002454 } else if (contentLength == -1) {
2455 // When no Content-length (default value == -1), attempt to read a byte
2456 // from the response. Do not use available() as it is unreliable.
2457 // See http://b/33498325.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002458 if (urlConnection.getInputStream().read() == -1) {
Sehee Park43427c02018-11-16 17:39:34 +09002459 validationLog(probeType, url,
2460 "Empty 200 response interpreted as failed response.");
2461 httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002462 }
lucaslinc3d9f712020-04-10 03:28:12 +00002463 } else if (matchesHttpContentLength(contentLength)) {
2464 final InputStream is = new BufferedInputStream(urlConnection.getInputStream());
2465 final String content = readAsString(is, (int) contentLength,
2466 extractCharset(urlConnection.getContentType()));
2467 if (matchesHttpContent(content,
2468 R.string.config_network_validation_failed_content_regexp)) {
2469 httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
2470 } else if (matchesHttpContent(content,
2471 R.string.config_network_validation_success_content_regexp)) {
2472 httpResponseCode = CaptivePortalProbeResult.SUCCESS_CODE;
2473 }
2474
2475 if (httpResponseCode != 200) {
2476 validationLog(probeType, url, "200 response with Content-length ="
2477 + contentLength + ", content matches custom regexp, interpreted"
2478 + " as " + httpResponseCode
2479 + " response.");
2480 }
Sehee Park43427c02018-11-16 17:39:34 +09002481 } else if (contentLength <= 4) {
2482 // Consider 200 response with "Content-length <= 4" to not be a captive
2483 // portal. There's no point in considering this a captive portal as the
2484 // user cannot sign-in to an empty page. Probably the result of a broken
2485 // transparent proxy. See http://b/9972012 and http://b/122999481.
2486 validationLog(probeType, url, "200 response with Content-length <= 4"
2487 + " interpreted as failed response.");
2488 httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002489 }
2490 }
2491 } catch (IOException e) {
2492 validationLog(probeType, url, "Probe failed with exception " + e);
2493 if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
2494 // TODO: Ping gateway and DNS server and log results.
2495 }
2496 } finally {
2497 if (urlConnection != null) {
2498 urlConnection.disconnect();
2499 }
2500 TrafficStats.setThreadStatsTag(oldTag);
2501 }
2502 logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
2503
Frank Lie04f8b72020-06-22 12:45:27 +00002504 final CaptivePortalProbeResult probeResult;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002505 if (probeSpec == null) {
Frank Lie04f8b72020-06-22 12:45:27 +00002506 probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl,
2507 url.toString(), 1 << probeType);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002508 } else {
Frank Lie04f8b72020-06-22 12:45:27 +00002509 probeResult = probeSpec.getResult(httpResponseCode, redirectUrl);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002510 }
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09002511 recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
Frank Lie04f8b72020-06-22 12:45:27 +00002512 probeTimer.stop(), NetworkValidationMetrics.httpProbeResultToEnum(probeResult),
2513 null /* capportData */);
2514 return probeResult;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002515 }
2516
lucaslinc3d9f712020-04-10 03:28:12 +00002517 @VisibleForTesting
2518 boolean matchesHttpContent(final String content, @StringRes final int configResource) {
2519 final String resString = getResStringConfig(mContext, configResource, "");
2520 try {
2521 return content.matches(resString);
2522 } catch (PatternSyntaxException e) {
2523 Log.e(TAG, "Pattern syntax exception occurs when matching the resource=" + resString,
2524 e);
2525 return false;
2526 }
2527 }
2528
2529 @VisibleForTesting
2530 boolean matchesHttpContentLength(final long contentLength) {
2531 // Consider that the Resources#getInteger() is returning an integer, so if the contentLength
2532 // is lower or equal to 0 or higher than Integer.MAX_VALUE, then it's an invalid value.
2533 if (contentLength <= 0) return false;
2534 if (contentLength > Integer.MAX_VALUE) {
2535 logw("matchesHttpContentLength : Get invalid contentLength = " + contentLength);
2536 return false;
2537 }
2538 return (contentLength > getResIntConfig(mContext,
2539 R.integer.config_min_matches_http_content_length, Integer.MAX_VALUE)
2540 &&
2541 contentLength < getResIntConfig(mContext,
2542 R.integer.config_max_matches_http_content_length, 0));
2543 }
2544
Remi NGUYEN VAN2d909a72019-12-24 18:15:52 +09002545 private HttpURLConnection makeProbeConnection(URL url, boolean followRedirects)
2546 throws IOException {
2547 final HttpURLConnection conn = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
2548 conn.setInstanceFollowRedirects(followRedirects);
2549 conn.setConnectTimeout(SOCKET_TIMEOUT_MS);
2550 conn.setReadTimeout(SOCKET_TIMEOUT_MS);
2551 conn.setRequestProperty("Connection", "close");
2552 conn.setUseCaches(false);
2553 if (mCaptivePortalUserAgent != null) {
2554 conn.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
2555 }
2556 return conn;
2557 }
2558
2559 @VisibleForTesting
2560 @NonNull
2561 protected static String readAsString(InputStream is, int maxLength, Charset charset)
2562 throws IOException {
2563 final InputStreamReader reader = new InputStreamReader(is, charset);
2564 final char[] buffer = new char[1000];
2565 final StringBuilder builder = new StringBuilder();
2566 int totalReadLength = 0;
2567 while (totalReadLength < maxLength) {
2568 final int availableLength = Math.min(maxLength - totalReadLength, buffer.length);
2569 final int currentLength = reader.read(buffer, 0, availableLength);
2570 if (currentLength < 0) break; // EOF
2571
2572 totalReadLength += currentLength;
2573 builder.append(buffer, 0, currentLength);
2574 }
2575 return builder.toString();
2576 }
2577
2578 /**
2579 * Attempt to extract the {@link Charset} of the response from its Content-Type header.
2580 *
2581 * <p>If the {@link Charset} cannot be extracted, UTF-8 is returned by default.
2582 */
2583 @VisibleForTesting
2584 @NonNull
2585 protected static Charset extractCharset(@Nullable String contentTypeHeader) {
2586 if (contentTypeHeader == null) return StandardCharsets.UTF_8;
2587 // See format in https://tools.ietf.org/html/rfc7231#section-3.1.1.1
2588 final Pattern charsetPattern = Pattern.compile("; *charset=\"?([^ ;\"]+)\"?",
2589 Pattern.CASE_INSENSITIVE);
2590 final Matcher matcher = charsetPattern.matcher(contentTypeHeader);
2591 if (!matcher.find()) return StandardCharsets.UTF_8;
2592
2593 try {
2594 return Charset.forName(matcher.group(1));
2595 } catch (IllegalArgumentException e) {
2596 return StandardCharsets.UTF_8;
2597 }
2598 }
2599
Chiachang Wang50865812020-04-14 16:26:24 +00002600 private class ProbeThread extends Thread {
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002601 private final CountDownLatch mLatch;
Chiachang Wang50865812020-04-14 16:26:24 +00002602 private final Probe mProbe;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002603
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002604 ProbeThread(CountDownLatch latch, EvaluationThreadDeps deps, ProxyInfo proxy, URL url,
2605 int probeType, Uri captivePortalApiUrl) {
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002606 mLatch = latch;
Chiachang Wang50865812020-04-14 16:26:24 +00002607 mProbe = (probeType == ValidationProbeEvent.PROBE_HTTPS)
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002608 ? new HttpsProbe(deps, proxy, url, captivePortalApiUrl)
2609 : new HttpProbe(deps, proxy, url, captivePortalApiUrl);
Chiachang Wang50865812020-04-14 16:26:24 +00002610 mResult = CaptivePortalProbeResult.failed(probeType);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002611 }
2612
Chiachang Wang50865812020-04-14 16:26:24 +00002613 private volatile CaptivePortalProbeResult mResult;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002614
2615 public CaptivePortalProbeResult result() {
2616 return mResult;
2617 }
2618
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002619 @Override
2620 public void run() {
Chiachang Wang50865812020-04-14 16:26:24 +00002621 mResult = mProbe.sendProbe();
2622 if (isConclusiveResult(mResult, mProbe.mCaptivePortalApiUrl)) {
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002623 // Stop waiting immediately if any probe is conclusive.
2624 while (mLatch.getCount() > 0) {
2625 mLatch.countDown();
2626 }
2627 }
2628 // Signal this probe has completed.
2629 mLatch.countDown();
2630 }
2631 }
2632
Chiachang Wang50865812020-04-14 16:26:24 +00002633 private abstract static class Probe {
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002634 protected final EvaluationThreadDeps mDeps;
Chiachang Wang50865812020-04-14 16:26:24 +00002635 protected final ProxyInfo mProxy;
2636 protected final URL mUrl;
2637 protected final Uri mCaptivePortalApiUrl;
2638
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002639 protected Probe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url,
2640 Uri captivePortalApiUrl) {
2641 mDeps = deps;
Chiachang Wang50865812020-04-14 16:26:24 +00002642 mProxy = proxy;
2643 mUrl = url;
2644 mCaptivePortalApiUrl = captivePortalApiUrl;
2645 }
2646 // sendProbe() is synchronous and blocks until it has the result.
2647 protected abstract CaptivePortalProbeResult sendProbe();
2648 }
2649
2650 final class HttpsProbe extends Probe {
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002651 HttpsProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
2652 super(deps, proxy, url, captivePortalApiUrl);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002653 }
2654
2655 @Override
Chiachang Wang50865812020-04-14 16:26:24 +00002656 protected CaptivePortalProbeResult sendProbe() {
2657 return sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTPS);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002658 }
2659 }
2660
Chiachang Wang50865812020-04-14 16:26:24 +00002661 final class HttpProbe extends Probe {
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002662 HttpProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
2663 super(deps, proxy, url, captivePortalApiUrl);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002664 }
2665
Frank Lie04f8b72020-06-22 12:45:27 +00002666 private CaptivePortalDataShim sendCapportApiProbe() {
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09002667 // TODO: consider adding metrics counters for each case returning null in this method
2668 // (cases where the API is not implemented properly).
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002669 validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl);
2670
2671 final String apiContent;
2672 try {
2673 final URL url = new URL(mCaptivePortalApiUrl.toString());
Remi NGUYEN VAN6bed0e52020-05-20 10:33:03 +00002674 // Protocol must be HTTPS
2675 // (as per https://www.ietf.org/id/draft-ietf-capport-api-07.txt, #4).
2676 // Only allow HTTP on localhost, for testing.
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002677 final boolean isTestLocalhostHttp = mDeps.mIsTestNetwork
2678 && "localhost".equals(url.getHost()) && "http".equals(url.getProtocol());
2679 if (!"https".equals(url.getProtocol()) && !isTestLocalhostHttp) {
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002680 validationLog("Invalid captive portal API protocol: " + url.getProtocol());
2681 return null;
2682 }
2683
2684 final HttpURLConnection conn = makeProbeConnection(
2685 url, true /* followRedirects */);
2686 conn.setRequestProperty(ACCEPT_HEADER, CAPPORT_API_CONTENT_TYPE);
2687 final int responseCode = conn.getResponseCode();
2688 if (responseCode != 200) {
2689 validationLog("Non-200 API response code: " + conn.getResponseCode());
2690 return null;
2691 }
2692 final Charset charset = extractCharset(conn.getHeaderField(CONTENT_TYPE_HEADER));
2693 if (charset != StandardCharsets.UTF_8) {
2694 validationLog("Invalid charset for capport API: " + charset);
2695 return null;
2696 }
2697
2698 apiContent = readAsString(conn.getInputStream(),
2699 CAPPORT_API_MAX_JSON_LENGTH, charset);
2700 } catch (IOException e) {
2701 validationLog("I/O error reading capport data: " + e.getMessage());
2702 return null;
2703 }
2704
2705 try {
2706 final JSONObject info = new JSONObject(apiContent);
Frank Lie04f8b72020-06-22 12:45:27 +00002707 final CaptivePortalDataShim capportData = CaptivePortalDataShimImpl.fromJson(info);
2708 if (capportData != null && capportData.isCaptive()
2709 && capportData.getUserPortalUrl() == null) {
2710 validationLog("Missing user-portal-url from capport response");
2711 return null;
2712 }
2713 return capportData;
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002714 } catch (JSONException e) {
2715 validationLog("Could not parse capport API JSON: " + e.getMessage());
2716 return null;
2717 } catch (UnsupportedApiLevelException e) {
Frank Lie04f8b72020-06-22 12:45:27 +00002718 // This should never happen because LinkProperties would not have a capport URL
2719 // before R.
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002720 validationLog("Platform API too low to support capport API");
2721 return null;
2722 }
2723 }
2724
Frank Lie04f8b72020-06-22 12:45:27 +00002725 private CaptivePortalDataShim tryCapportApiProbe() {
2726 if (mCaptivePortalApiUrl == null) return null;
2727 final Stopwatch capportApiWatch = new Stopwatch().start();
2728 final CaptivePortalDataShim capportData = sendCapportApiProbe();
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09002729 recordProbeEventMetrics(ProbeType.PT_CAPPORT_API, capportApiWatch.stop(),
Frank Lie04f8b72020-06-22 12:45:27 +00002730 capportData == null ? ProbeResult.PR_FAILURE : ProbeResult.PR_SUCCESS,
2731 capportData);
2732 return capportData;
2733 }
2734
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002735 @Override
Chiachang Wang50865812020-04-14 16:26:24 +00002736 protected CaptivePortalProbeResult sendProbe() {
2737 final CaptivePortalDataShim capportData = tryCapportApiProbe();
2738 if (capportData != null && capportData.isCaptive()) {
Chiachang Wang50865812020-04-14 16:26:24 +00002739 final String loginUrlString = capportData.getUserPortalUrl().toString();
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002740 // Starting from R (where CaptivePortalData was introduced), the captive portal app
2741 // delegates to NetworkMonitor for verifying when the network validates instead of
2742 // probing the detectUrl. So pass the detectUrl to have the portal open on that,
2743 // page; CaptivePortalLogin will not use it for probing.
Chiachang Wang50865812020-04-14 16:26:24 +00002744 return new CapportApiProbeResult(
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002745 CaptivePortalProbeResult.PORTAL_CODE,
2746 loginUrlString /* redirectUrl */,
Chiachang Wang50865812020-04-14 16:26:24 +00002747 loginUrlString /* detectUrl */,
2748 capportData,
2749 1 << ValidationProbeEvent.PROBE_HTTP);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002750 }
2751
2752 // If the API says it's not captive, still check for HTTP connectivity. This helps
2753 // with partial connectivity detection, and a broken API saying that there is no
2754 // redirect when there is one.
Chiachang Wang50865812020-04-14 16:26:24 +00002755 final CaptivePortalProbeResult res =
2756 sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTP);
Chiachang Wang3278b382020-04-16 13:04:02 +00002757 return mCaptivePortalApiUrl == null ? res : new CapportApiProbeResult(res, capportData);
Chiachang Wang50865812020-04-14 16:26:24 +00002758 }
2759 }
2760
2761 private static boolean isConclusiveResult(@NonNull CaptivePortalProbeResult result,
2762 @Nullable Uri captivePortalApiUrl) {
2763 // isPortal() is not expected on the HTTPS probe, but treat the network as portal would make
2764 // sense if the probe reports portal. In case the capport API is available, the API is
2765 // authoritative on whether there is a portal, so the HTTPS probe is not enough to conclude
2766 // there is connectivity, and a determination will be made once the capport API probe
2767 // returns. Note that the API can only force the system to detect a portal even if the HTTPS
2768 // probe succeeds. It cannot force the system to detect no portal if the HTTPS probe fails.
2769 return result.isPortal()
2770 || (result.isConcludedFromHttps() && result.isSuccessful()
2771 && captivePortalApiUrl == null);
2772 }
2773
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002774 private CaptivePortalProbeResult sendMultiParallelHttpAndHttpsProbes(
2775 @NonNull EvaluationThreadDeps deps, @Nullable ProxyInfo proxy, @NonNull URL[] httpsUrls,
2776 @NonNull URL[] httpUrls) {
Chiachang Wang3278b382020-04-16 13:04:02 +00002777 // If multiple URLs are required to ensure the correctness of validation, send parallel
2778 // probes to explore the result in separate probe threads and aggregate those results into
2779 // one as the final result for either HTTP or HTTPS.
2780
2781 // Number of probes to wait for.
2782 final int num = httpsUrls.length + httpUrls.length;
2783 // Fixed pool to prevent configuring too many urls to exhaust system resource.
2784 final ExecutorService executor = Executors.newFixedThreadPool(
2785 Math.min(num, MAX_PROBE_THREAD_POOL_SIZE));
2786 final CompletionService<CaptivePortalProbeResult> ecs =
2787 new ExecutorCompletionService<CaptivePortalProbeResult>(executor);
2788 final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
2789 final List<Future<CaptivePortalProbeResult>> futures = new ArrayList<>();
2790
2791 try {
2792 // Queue https and http probe.
2793
2794 // Each of these HTTP probes will start with probing capport API if present. So if
2795 // multiple HTTP URLs are configured, AP will send multiple identical accesses to the
2796 // capport URL. Thus, send capport API probing with one of the HTTP probe is enough.
2797 // Probe capport API with the first HTTP probe.
2798 // TODO: Have the capport probe as a different probe for cleanliness.
2799 final URL urlMaybeWithCapport = httpUrls[0];
2800 for (final URL url : httpUrls) {
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002801 futures.add(ecs.submit(() -> new HttpProbe(deps, proxy, url,
Chiachang Wang3278b382020-04-16 13:04:02 +00002802 url.equals(urlMaybeWithCapport) ? capportApiUrl : null).sendProbe()));
2803 }
2804
2805 for (final URL url : httpsUrls) {
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002806 futures.add(ecs.submit(() -> new HttpsProbe(deps, proxy, url, capportApiUrl)
2807 .sendProbe()));
Chiachang Wang3278b382020-04-16 13:04:02 +00002808 }
2809
2810 final ArrayList<CaptivePortalProbeResult> completedProbes = new ArrayList<>();
2811 for (int i = 0; i < num; i++) {
2812 completedProbes.add(ecs.take().get());
2813 final CaptivePortalProbeResult res = evaluateCapportResult(
2814 completedProbes, httpsUrls.length, capportApiUrl != null /* hasCapport */);
2815 if (res != null) {
2816 reportProbeResult(res);
2817 return res;
2818 }
2819 }
2820 } catch (ExecutionException e) {
2821 Log.e(TAG, "Error sending probes.", e);
2822 } catch (InterruptedException e) {
2823 // Ignore interrupted probe result because result is not important to conclude the
2824 // result.
2825 } finally {
2826 // Interrupt ongoing probes since we have already gotten result from one of them.
2827 futures.forEach(future -> future.cancel(true));
2828 executor.shutdownNow();
2829 }
2830
2831 return CaptivePortalProbeResult.failed(ValidationProbeEvent.PROBE_HTTPS);
2832 }
2833
2834 @Nullable
2835 private CaptivePortalProbeResult evaluateCapportResult(
2836 List<CaptivePortalProbeResult> probes, int numHttps, boolean hasCapport) {
2837 CaptivePortalProbeResult capportResult = null;
2838 CaptivePortalProbeResult httpPortalResult = null;
2839 int httpSuccesses = 0;
2840 int httpsSuccesses = 0;
2841 int httpsFailures = 0;
2842
2843 for (CaptivePortalProbeResult probe : probes) {
2844 if (probe instanceof CapportApiProbeResult) {
2845 capportResult = probe;
2846 } else if (probe.isConcludedFromHttps()) {
2847 if (probe.isSuccessful()) httpsSuccesses++;
2848 else httpsFailures++;
2849 } else { // http probes
2850 if (probe.isPortal()) {
2851 // Unlike https probe, http probe may have redirect url information kept in the
2852 // probe result. Thus, the result can not be newly created with response code
2853 // only. If the captive portal behavior will be varied because of different
2854 // probe URLs, this means that if the portal returns different redirect URLs for
2855 // different probes and has a different behavior depending on the URL, then the
2856 // behavior of the login page may differ depending on the order in which the
2857 // probes terminate. However, NetworkMonitor does have to choose one of the
2858 // redirect URLs and right now there is no clue at all which of the probe has
2859 // the better redirect URL, so there is no telling which is best to use.
2860 // Therefore the current code just uses whichever happens to be the last one to
2861 // complete.
2862 httpPortalResult = probe;
2863 } else if (probe.isSuccessful()) {
2864 httpSuccesses++;
2865 }
2866 }
2867 }
2868 // If there is Capport url configured but the result is not available yet, wait for it.
2869 if (hasCapport && capportResult == null) return null;
2870 // Capport API saying it's a portal is authoritative.
2871 if (capportResult != null && capportResult.isPortal()) return capportResult;
2872 // Any HTTP probes saying probe portal is conclusive.
2873 if (httpPortalResult != null) return httpPortalResult;
2874 // Any HTTPS probes works then the network validates.
2875 if (httpsSuccesses > 0) {
2876 return CaptivePortalProbeResult.success(1 << ValidationProbeEvent.PROBE_HTTPS);
2877 }
2878 // All HTTPS failed and at least one HTTP succeeded, then it's partial.
2879 if (httpsFailures == numHttps && httpSuccesses > 0) {
2880 return CaptivePortalProbeResult.PARTIAL;
2881 }
2882 // Otherwise, the result is unknown yet.
2883 return null;
2884 }
2885
Chiachang Wang50865812020-04-14 16:26:24 +00002886 private void reportProbeResult(@NonNull CaptivePortalProbeResult res) {
2887 if (res instanceof CapportApiProbeResult) {
2888 maybeReportCaptivePortalData(((CapportApiProbeResult) res).getCaptivePortalData());
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002889 }
2890
Chiachang Wang50865812020-04-14 16:26:24 +00002891 // This is not a if-else case since partial connectivity will concluded from both HTTP and
2892 // HTTPS probe. Both HTTP and HTTPS result should be reported.
2893 if (res.isConcludedFromHttps()) {
2894 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, res);
2895 }
2896
2897 if (res.isConcludedFromHttp()) {
2898 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, res);
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002899 }
2900 }
2901
Chiachang Wang3278b382020-04-16 13:04:02 +00002902 private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002903 EvaluationThreadDeps deps, ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002904 // Number of probes to wait for. If a probe completes with a conclusive answer
2905 // it shortcuts the latch immediately by forcing the count to 0.
2906 final CountDownLatch latch = new CountDownLatch(2);
2907
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002908 final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002909 final ProbeThread httpsProbe = new ProbeThread(latch, deps, proxy, httpsUrl,
Chiachang Wang50865812020-04-14 16:26:24 +00002910 ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
Remi NGUYEN VANe443dd62020-05-29 08:24:08 +00002911 final ProbeThread httpProbe = new ProbeThread(latch, deps, proxy, httpUrl,
Chiachang Wang50865812020-04-14 16:26:24 +00002912 ValidationProbeEvent.PROBE_HTTP, capportApiUrl);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002913
2914 try {
2915 httpsProbe.start();
2916 httpProbe.start();
2917 latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
2918 } catch (InterruptedException e) {
2919 validationLog("Error: probes wait interrupted!");
Chiachang Wang50865812020-04-14 16:26:24 +00002920 return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002921 }
2922
2923 final CaptivePortalProbeResult httpsResult = httpsProbe.result();
2924 final CaptivePortalProbeResult httpResult = httpProbe.result();
2925
2926 // Look for a conclusive probe result first.
Chiachang Wang50865812020-04-14 16:26:24 +00002927 if (isConclusiveResult(httpResult, capportApiUrl)) {
2928 reportProbeResult(httpProbe.result());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002929 return httpResult;
2930 }
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09002931
Chiachang Wang50865812020-04-14 16:26:24 +00002932 if (isConclusiveResult(httpsResult, capportApiUrl)) {
2933 reportProbeResult(httpsProbe.result());
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002934 return httpsResult;
2935 }
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002936 // Consider a DNS response with a private IP address on the HTTP probe as an indication that
2937 // the network is not connected to the Internet, and have the whole evaluation fail in that
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00002938 // case, instead of potentially detecting a captive portal. This logic only affects portal
2939 // detection, not network validation.
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002940 // This only applies if the DNS probe completed within PROBE_TIMEOUT_MS, as the fallback
2941 // probe should not be delayed by this check.
Remi NGUYEN VAN3ca1ff12020-04-10 10:26:26 +00002942 if (mPrivateIpNoInternetEnabled && (httpResult.isDnsPrivateIpResponse())) {
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002943 validationLog("DNS response to the URL is private IP");
Chiachang Wang50865812020-04-14 16:26:24 +00002944 return CaptivePortalProbeResult.failed(1 << ValidationProbeEvent.PROBE_HTTP);
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00002945 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002946 // If a fallback method exists, use it to retry portal detection.
2947 // If we have new-style probe specs, use those. Otherwise, use the fallback URLs.
2948 final CaptivePortalProbeSpec probeSpec = nextFallbackSpec();
2949 final URL fallbackUrl = (probeSpec != null) ? probeSpec.getUrl() : nextFallbackUrl();
lucaslinb0573962019-03-12 13:08:03 +08002950 CaptivePortalProbeResult fallbackProbeResult = null;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002951 if (fallbackUrl != null) {
lucaslinb0573962019-03-12 13:08:03 +08002952 fallbackProbeResult = sendHttpProbe(fallbackUrl, PROBE_FALLBACK, probeSpec);
Chiachang Wang813ee472019-05-23 16:29:30 +08002953 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_FALLBACK, fallbackProbeResult);
lucaslinb0573962019-03-12 13:08:03 +08002954 if (fallbackProbeResult.isPortal()) {
2955 return fallbackProbeResult;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002956 }
2957 }
2958 // Otherwise wait until http and https probes completes and use their results.
2959 try {
2960 httpProbe.join();
Chiachang Wang50865812020-04-14 16:26:24 +00002961 reportProbeResult(httpProbe.result());
Chiachang Wang813ee472019-05-23 16:29:30 +08002962
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002963 if (httpProbe.result().isPortal()) {
2964 return httpProbe.result();
2965 }
Chiachang Wang813ee472019-05-23 16:29:30 +08002966
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002967 httpsProbe.join();
Chiachang Wang813ee472019-05-23 16:29:30 +08002968 reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());
2969
lucaslinb0573962019-03-12 13:08:03 +08002970 final boolean isHttpSuccessful =
2971 (httpProbe.result().isSuccessful()
2972 || (fallbackProbeResult != null && fallbackProbeResult.isSuccessful()));
2973 if (httpsProbe.result().isFailed() && isHttpSuccessful) {
2974 return CaptivePortalProbeResult.PARTIAL;
2975 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002976 return httpsProbe.result();
2977 } catch (InterruptedException e) {
2978 validationLog("Error: http or https probe wait interrupted!");
Chiachang Wang50865812020-04-14 16:26:24 +00002979 return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002980 }
2981 }
2982
2983 private URL makeURL(String url) {
2984 if (url != null) {
2985 try {
2986 return new URL(url);
2987 } catch (MalformedURLException e) {
2988 validationLog("Bad URL: " + url);
2989 }
2990 }
2991 return null;
2992 }
2993
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002994 private void logNetworkEvent(int evtype) {
2995 int[] transports = mNetworkCapabilities.getTransportTypes();
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09002996 mMetricsLog.log(mCleartextDnsNetwork, transports, new NetworkEvent(evtype));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09002997 }
2998
2999 private int networkEventType(ValidationStage s, EvaluationResult r) {
3000 if (s.mIsFirstValidation) {
3001 if (r.mIsValidated) {
3002 return NetworkEvent.NETWORK_FIRST_VALIDATION_SUCCESS;
3003 } else {
3004 return NetworkEvent.NETWORK_FIRST_VALIDATION_PORTAL_FOUND;
3005 }
3006 } else {
3007 if (r.mIsValidated) {
3008 return NetworkEvent.NETWORK_REVALIDATION_SUCCESS;
3009 } else {
3010 return NetworkEvent.NETWORK_REVALIDATION_PORTAL_FOUND;
3011 }
3012 }
3013 }
3014
3015 private void maybeLogEvaluationResult(int evtype) {
3016 if (mEvaluationTimer.isRunning()) {
3017 int[] transports = mNetworkCapabilities.getTransportTypes();
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09003018 mMetricsLog.log(mCleartextDnsNetwork, transports,
Frank Lifa914142020-05-18 04:48:03 +00003019 new NetworkEvent(evtype, mEvaluationTimer.stop() / 1000));
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003020 mEvaluationTimer.reset();
3021 }
3022 }
3023
Frank Lifa914142020-05-18 04:48:03 +00003024 private void logValidationProbe(long durationUs, int probeType, int probeResult) {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003025 int[] transports = mNetworkCapabilities.getTransportTypes();
3026 boolean isFirstValidation = validationStage().mIsFirstValidation;
Remi NGUYEN VANc148d8b2019-01-19 21:13:24 +09003027 ValidationProbeEvent ev = new ValidationProbeEvent.Builder()
3028 .setProbeType(probeType, isFirstValidation)
3029 .setReturnCode(probeResult)
Frank Lifa914142020-05-18 04:48:03 +00003030 .setDurationMs(durationUs / 1000)
Remi NGUYEN VANc148d8b2019-01-19 21:13:24 +09003031 .build();
Lorenzo Colitti7f9734f2019-05-09 12:13:54 +09003032 mMetricsLog.log(mCleartextDnsNetwork, transports, ev);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003033 }
3034
3035 @VisibleForTesting
Remi NGUYEN VANea9f7e32019-06-20 18:49:48 +09003036 public static class Dependencies {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003037 public Network getPrivateDnsBypassNetwork(Network network) {
3038 return new OneAddressPerFamilyNetwork(network);
3039 }
3040
Lorenzo Colitti171cfd22019-04-18 13:44:32 +09003041 public DnsResolver getDnsResolver() {
3042 return DnsResolver.getInstance();
3043 }
3044
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003045 public Random getRandom() {
3046 return new Random();
3047 }
3048
3049 /**
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003050 * Get the value of a global integer setting.
3051 * @param symbol Name of the setting
3052 * @param defaultValue Value to return if the setting is not defined.
3053 */
3054 public int getSetting(Context context, String symbol, int defaultValue) {
3055 return Settings.Global.getInt(context.getContentResolver(), symbol, defaultValue);
3056 }
3057
3058 /**
3059 * Get the value of a global String setting.
3060 * @param symbol Name of the setting
3061 * @param defaultValue Value to return if the setting is not defined.
3062 */
3063 public String getSetting(Context context, String symbol, String defaultValue) {
3064 final String value = Settings.Global.getString(context.getContentResolver(), symbol);
3065 return value != null ? value : defaultValue;
3066 }
3067
Chiachang Wang9a87f802019-04-08 19:06:21 +08003068 /**
3069 * Look up the value of a property in DeviceConfig.
3070 * @param namespace The namespace containing the property to look up.
3071 * @param name The name of the property to look up.
3072 * @param defaultValue The value to return if the property does not exist or has no non-null
3073 * value.
3074 * @return the corresponding value, or defaultValue if none exists.
3075 */
3076 @Nullable
3077 public String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
3078 @Nullable String defaultValue) {
3079 return NetworkStackUtils.getDeviceConfigProperty(namespace, name, defaultValue);
3080 }
3081
3082 /**
3083 * Look up the value of a property in DeviceConfig.
3084 * @param namespace The namespace containing the property to look up.
3085 * @param name The name of the property to look up.
3086 * @param defaultValue The value to return if the property does not exist or has no non-null
3087 * value.
3088 * @return the corresponding value, or defaultValue if none exists.
3089 */
3090 public int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
3091 int defaultValue) {
3092 return NetworkStackUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
3093 }
3094
Remi NGUYEN VANea9f7e32019-06-20 18:49:48 +09003095 /**
Remi NGUYEN VAN75e9d902020-04-09 06:41:16 +00003096 * Check whether or not one experimental feature in the connectivity namespace is
3097 * enabled.
3098 * @param name Flag name of the experiment in the connectivity namespace.
3099 * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
3100 */
3101 public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
3102 return NetworkStackUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
3103 }
3104
3105 /**
Automerger Merge Workerfaac06e2020-03-09 09:23:38 +00003106 * Check whether or not one specific experimental feature for a particular namespace from
3107 * {@link DeviceConfig} is enabled by comparing NetworkStack module version
3108 * {@link NetworkStack} with current version of property. If this property version is valid,
3109 * the corresponding experimental feature would be enabled, otherwise disabled.
3110 * @param context The global context information about an app environment.
3111 * @param namespace The namespace containing the property to look up.
3112 * @param name The name of the property to look up.
3113 * @param defaultEnabled The value to return if the property does not exist or its value is
3114 * null.
3115 * @return true if this feature is enabled, or false if disabled.
3116 */
3117 public boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
3118 @NonNull String name, boolean defaultEnabled) {
3119 return NetworkStackUtils.isFeatureEnabled(context, namespace, name, defaultEnabled);
3120 }
3121
Chiachang Wang9b105a92020-03-30 09:12:37 +00003122 /**
3123 * Collect data stall detection level information for each transport type. Write metrics
3124 * data to statsd pipeline.
3125 * @param stats a {@link DataStallDetectionStats} that contains the detection level
3126 * information.
3127 * @para result the network reevaluation result.
3128 */
3129 public void writeDataStallDetectionStats(@NonNull final DataStallDetectionStats stats,
3130 @NonNull final CaptivePortalProbeResult result) {
3131 DataStallStatsUtils.write(stats, result);
3132 }
3133
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003134 public static final Dependencies DEFAULT = new Dependencies();
3135 }
3136
3137 /**
3138 * Methods in this class perform no locking because all accesses are performed on the state
3139 * machine's thread. Need to consider the thread safety if it ever could be accessed outside the
3140 * state machine.
3141 */
3142 @VisibleForTesting
3143 protected class DnsStallDetector {
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003144 private int mConsecutiveTimeoutCount = 0;
3145 private int mSize;
3146 final DnsResult[] mDnsEvents;
3147 final RingBufferIndices mResultIndices;
3148
3149 DnsStallDetector(int size) {
3150 mSize = Math.max(DEFAULT_DNS_LOG_SIZE, size);
3151 mDnsEvents = new DnsResult[mSize];
3152 mResultIndices = new RingBufferIndices(mSize);
3153 }
3154
3155 @VisibleForTesting
3156 protected void accumulateConsecutiveDnsTimeoutCount(int code) {
3157 final DnsResult result = new DnsResult(code);
3158 mDnsEvents[mResultIndices.add()] = result;
3159 if (result.isTimeout()) {
3160 mConsecutiveTimeoutCount++;
3161 } else {
3162 // Keep the event in mDnsEvents without clearing it so that there are logs to do the
3163 // simulation and analysis.
3164 mConsecutiveTimeoutCount = 0;
3165 }
3166 }
3167
3168 private boolean isDataStallSuspected(int timeoutCountThreshold, int validTime) {
3169 if (timeoutCountThreshold <= 0) {
3170 Log.wtf(TAG, "Timeout count threshold should be larger than 0.");
3171 return false;
3172 }
3173
3174 // Check if the consecutive timeout count reach the threshold or not.
3175 if (mConsecutiveTimeoutCount < timeoutCountThreshold) {
3176 return false;
3177 }
3178
3179 // Check if the target dns event index is valid or not.
3180 final int firstConsecutiveTimeoutIndex =
3181 mResultIndices.indexOf(mResultIndices.size() - timeoutCountThreshold);
3182
3183 // If the dns timeout events happened long time ago, the events are meaningless for
3184 // data stall evaluation. Thus, check if the first consecutive timeout dns event
3185 // considered in the evaluation happened in defined threshold time.
3186 final long now = SystemClock.elapsedRealtime();
3187 final long firstTimeoutTime = now - mDnsEvents[firstConsecutiveTimeoutIndex].mTimeStamp;
3188 return (firstTimeoutTime < validTime);
3189 }
3190
3191 int getConsecutiveTimeoutCount() {
3192 return mConsecutiveTimeoutCount;
3193 }
3194 }
3195
3196 private static class DnsResult {
3197 // TODO: Need to move the DNS return code definition to a specific class once unify DNS
3198 // response code is done.
3199 private static final int RETURN_CODE_DNS_TIMEOUT = 255;
3200
3201 private final long mTimeStamp;
3202 private final int mReturnCode;
3203
3204 DnsResult(int code) {
3205 mTimeStamp = SystemClock.elapsedRealtime();
3206 mReturnCode = code;
3207 }
3208
3209 private boolean isTimeout() {
3210 return mReturnCode == RETURN_CODE_DNS_TIMEOUT;
3211 }
3212 }
3213
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003214 @VisibleForTesting
Chiachang Wang8e232042020-01-17 18:01:59 +08003215 @Nullable
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003216 protected DnsStallDetector getDnsStallDetector() {
3217 return mDnsStallDetector;
3218 }
3219
Chiachang Wange797c9b2019-11-28 14:18:47 +08003220 @Nullable
Chiachang Wang0b34ae62020-05-21 07:03:23 +00003221 private TcpSocketTracker getTcpSocketTracker() {
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003222 return mTcpTracker;
3223 }
3224
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003225 private boolean dataStallEvaluateTypeEnabled(int type) {
Chiachang Wang0e874792019-03-05 20:31:57 +08003226 return (mDataStallEvaluationType & type) != 0;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003227 }
3228
3229 @VisibleForTesting
3230 protected long getLastProbeTime() {
3231 return mLastProbeTime;
3232 }
3233
3234 @VisibleForTesting
3235 protected boolean isDataStall() {
Chiachang Wangb6dbfb42020-01-21 14:47:34 +08003236 if (!isValidationRequired()) {
3237 return false;
3238 }
3239
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003240 Boolean result = null;
Chiachang Wangd740dc32020-01-14 20:49:17 +08003241 final StringJoiner msg = (DBG || VDBG_STALL) ? new StringJoiner(", ") : null;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003242 // Reevaluation will generate traffic. Thus, set a minimal reevaluation timer to limit the
3243 // possible traffic cost in metered network.
Remi NGUYEN VANa0983f72019-01-20 16:50:42 +09003244 if (!mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003245 && (SystemClock.elapsedRealtime() - getLastProbeTime()
3246 < mDataStallMinEvaluateTime)) {
3247 return false;
3248 }
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003249 // Check TCP signal. Suspect it may be a data stall if :
3250 // 1. TCP connection fail rate(lost+retrans) is higher than threshold.
3251 // 2. Accumulate enough packets count.
Chiachang Wange797c9b2019-11-28 14:18:47 +08003252 final TcpSocketTracker tst = getTcpSocketTracker();
3253 if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_TCP) && tst != null) {
3254 if (tst.getLatestReceivedCount() > 0) {
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003255 result = false;
Chiachang Wange797c9b2019-11-28 14:18:47 +08003256 } else if (tst.isDataStallSuspected()) {
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003257 result = true;
Chiachang Wang0b34ae62020-05-21 07:03:23 +00003258 mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_TCP;
Cody Kesting782cbfa2020-01-06 15:51:59 -08003259
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003260 final DataStallReportParcelable p = new DataStallReportParcelable();
3261 p.detectionMethod = DETECTION_METHOD_TCP_METRICS;
3262 p.timestampMillis = SystemClock.elapsedRealtime();
3263 p.tcpPacketFailRate = tst.getLatestPacketFailPercentage();
3264 p.tcpMetricsCollectionPeriodMillis = getTcpPollingInterval();
3265
3266 notifyDataStallSuspected(p);
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003267 }
Chiachang Wangd740dc32020-01-14 20:49:17 +08003268 if (DBG || VDBG_STALL) {
Chiachang Wange797c9b2019-11-28 14:18:47 +08003269 msg.add("tcp packets received=" + tst.getLatestReceivedCount())
Chiachang Wangd740dc32020-01-14 20:49:17 +08003270 .add("latest tcp fail rate=" + tst.getLatestPacketFailPercentage());
Chiachang Wange797c9b2019-11-28 14:18:47 +08003271 }
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003272 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003273
3274 // Check dns signal. Suspect it may be a data stall if both :
Chiachang Wang0e874792019-03-05 20:31:57 +08003275 // 1. The number of consecutive DNS query timeouts >= mConsecutiveDnsTimeoutThreshold.
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003276 // 2. Those consecutive DNS queries happened in the last mValidDataStallDnsTimeThreshold ms.
Chiachang Wang8e232042020-01-17 18:01:59 +08003277 final DnsStallDetector dsd = getDnsStallDetector();
3278 if ((result == null) && (dsd != null)
3279 && dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_DNS)) {
3280 if (dsd.isDataStallSuspected(mConsecutiveDnsTimeoutThreshold,
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003281 mDataStallValidDnsTimeThreshold)) {
3282 result = true;
Chiachang Wang0b34ae62020-05-21 07:03:23 +00003283 mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_DNS;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003284 logNetworkEvent(NetworkEvent.NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND);
Cody Kesting782cbfa2020-01-06 15:51:59 -08003285
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003286 final DataStallReportParcelable p = new DataStallReportParcelable();
3287 p.detectionMethod = DETECTION_METHOD_DNS_EVENTS;
3288 p.timestampMillis = SystemClock.elapsedRealtime();
3289 p.dnsConsecutiveTimeouts = mDnsStallDetector.getConsecutiveTimeoutCount();
3290 notifyDataStallSuspected(p);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003291 }
Chiachang Wangd740dc32020-01-14 20:49:17 +08003292 if (DBG || VDBG_STALL) {
Chiachang Wang8e232042020-01-17 18:01:59 +08003293 msg.add("consecutive dns timeout count=" + dsd.getConsecutiveTimeoutCount());
Chiachang Wange797c9b2019-11-28 14:18:47 +08003294 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003295 }
Chiachang Wangd740dc32020-01-14 20:49:17 +08003296 // log only data stall suspected.
3297 if ((DBG && Boolean.TRUE.equals(result)) || VDBG_STALL) {
Chiachang Wange797c9b2019-11-28 14:18:47 +08003298 log("isDataStall: result=" + result + ", " + msg);
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003299 }
3300
Chiachang Wanga5716bf2019-11-20 16:13:07 +08003301 return (result == null) ? false : result;
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003302 }
Chiachang Wang813ee472019-05-23 16:29:30 +08003303
3304 // Class to keep state of evaluation results and probe results.
Lorenzo Colittiaa16f2d2019-06-07 18:42:34 +09003305 //
3306 // The main purpose was to ensure NetworkMonitor can notify ConnectivityService of probe results
Chiachang Wang813ee472019-05-23 16:29:30 +08003307 // as soon as they happen, without triggering any other changes. This requires keeping state on
Lorenzo Colittiaa16f2d2019-06-07 18:42:34 +09003308 // the most recent evaluation result. Calling noteProbeResult will ensure that the results
Chiachang Wang813ee472019-05-23 16:29:30 +08003309 // reported to ConnectivityService contain the previous evaluation result, and thus won't
3310 // trigger a validation or partial connectivity state change.
Lorenzo Colittiaa16f2d2019-06-07 18:42:34 +09003311 //
3312 // Note that this class is not currently being used for this purpose. The reason is that some
3313 // of the system behaviour triggered by reporting network validation - notably, NetworkAgent
3314 // behaviour - depends not only on the value passed by notifyNetworkTested, but also on the
3315 // fact that notifyNetworkTested was called. For example, telephony triggers network recovery
3316 // any time it is told that validation failed, i.e., if the result does not contain
3317 // NETWORK_VALIDATION_RESULT_VALID. But with this scheme, the first two or three validation
3318 // reports are all failures, because they are "HTTP succeeded but validation not yet passed",
3319 // "HTTP and HTTPS succeeded but validation not yet passed", etc.
Chiachang Wang813ee472019-05-23 16:29:30 +08003320 @VisibleForTesting
3321 protected class EvaluationState {
3322 // The latest validation result for this network. This is a bitmask of
3323 // INetworkMonitor.NETWORK_VALIDATION_RESULT_* constants.
3324 private int mEvaluationResult = NETWORK_VALIDATION_RESULT_INVALID;
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003325 // Indicates which probes have succeeded since clearProbeResults was called.
Chiachang Wang813ee472019-05-23 16:29:30 +08003326 // This is a bitmask of INetworkMonitor.NETWORK_VALIDATION_PROBE_* constants.
3327 private int mProbeResults = 0;
lucaslin2ce7dcc2019-10-22 16:59:39 +08003328 // A bitmask to record which probes are completed.
3329 private int mProbeCompleted = 0;
Chiachang Wang813ee472019-05-23 16:29:30 +08003330 // The latest redirect URL.
3331 private String mRedirectUrl;
3332
3333 protected void clearProbeResults() {
3334 mProbeResults = 0;
lucaslin2ce7dcc2019-10-22 16:59:39 +08003335 mProbeCompleted = 0;
Chiachang Wang813ee472019-05-23 16:29:30 +08003336 }
3337
lucaslin2ce7dcc2019-10-22 16:59:39 +08003338 private void maybeNotifyProbeResults(@NonNull final Runnable modif) {
3339 final int oldCompleted = mProbeCompleted;
3340 final int oldResults = mProbeResults;
3341 modif.run();
3342 if (oldCompleted != mProbeCompleted || oldResults != mProbeResults) {
3343 notifyProbeStatusChanged(mProbeCompleted, mProbeResults);
Chiachang Wang813ee472019-05-23 16:29:30 +08003344 }
Chiachang Wang813ee472019-05-23 16:29:30 +08003345 }
3346
lucaslin2ce7dcc2019-10-22 16:59:39 +08003347 protected void removeProbeResult(final int probeResult) {
3348 maybeNotifyProbeResults(() -> {
3349 mProbeCompleted &= ~probeResult;
3350 mProbeResults &= ~probeResult;
3351 });
3352 }
3353
3354 protected void noteProbeResult(final int probeResult, final boolean succeeded) {
3355 maybeNotifyProbeResults(() -> {
3356 mProbeCompleted |= probeResult;
3357 if (succeeded) {
3358 mProbeResults |= probeResult;
3359 } else {
3360 mProbeResults &= ~probeResult;
3361 }
3362 });
3363 }
3364
Chiachang Wang813ee472019-05-23 16:29:30 +08003365 protected void reportEvaluationResult(int result, @Nullable String redirectUrl) {
3366 mEvaluationResult = result;
3367 mRedirectUrl = redirectUrl;
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003368 final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
3369 p.result = result;
3370 p.probesSucceeded = mProbeResults;
3371 p.probesAttempted = mProbeCompleted;
3372 p.redirectUrl = redirectUrl;
3373 p.timestampMillis = SystemClock.elapsedRealtime();
3374 notifyNetworkTested(p);
Remi NGUYEN VAN7b6435f2020-06-26 16:39:48 +09003375 recordValidationResult(result, redirectUrl);
Chiachang Wang813ee472019-05-23 16:29:30 +08003376 }
3377
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003378 @VisibleForTesting
3379 protected int getEvaluationResult() {
3380 return mEvaluationResult;
3381 }
3382
3383 @VisibleForTesting
3384 protected int getProbeResults() {
3385 return mProbeResults;
Chiachang Wang813ee472019-05-23 16:29:30 +08003386 }
lucaslin2ce7dcc2019-10-22 16:59:39 +08003387
3388 @VisibleForTesting
3389 protected int getProbeCompletedResult() {
3390 return mProbeCompleted;
3391 }
Chiachang Wang813ee472019-05-23 16:29:30 +08003392 }
3393
3394 @VisibleForTesting
3395 protected EvaluationState getEvaluationState() {
3396 return mEvaluationState;
3397 }
3398
3399 private void maybeDisableHttpsProbing(boolean acceptPartial) {
3400 mAcceptPartialConnectivity = acceptPartial;
3401 // Ignore https probe in next validation if user accept partial connectivity on a partial
3402 // connectivity network.
Remi NGUYEN VAN812baf32020-04-24 12:19:37 +00003403 if (((mEvaluationState.getEvaluationResult() & NETWORK_VALIDATION_RESULT_PARTIAL) != 0)
Chiachang Wang813ee472019-05-23 16:29:30 +08003404 && mAcceptPartialConnectivity) {
3405 mUseHttps = false;
3406 }
3407 }
3408
3409 // Report HTTP, HTTP or FALLBACK probe result.
3410 @VisibleForTesting
3411 protected void reportHttpProbeResult(int probeResult,
3412 @NonNull final CaptivePortalProbeResult result) {
3413 boolean succeeded = result.isSuccessful();
3414 // The success of a HTTP probe does not tell us whether the DNS probe succeeded.
3415 // The DNS and HTTP probes run one after the other in sendDnsAndHttpProbes, and that
3416 // method cannot report the result of the DNS probe because that it could be running
3417 // on a different thread which is racing with the main state machine thread. So, if
3418 // an HTTP or HTTPS probe succeeded, assume that the DNS probe succeeded. But if an
3419 // HTTP or HTTPS probe failed, don't assume that DNS is not working.
3420 // TODO: fix this.
3421 if (succeeded) {
3422 probeResult |= NETWORK_VALIDATION_PROBE_DNS;
3423 }
Lorenzo Colittiaa16f2d2019-06-07 18:42:34 +09003424 mEvaluationState.noteProbeResult(probeResult, succeeded);
Chiachang Wang813ee472019-05-23 16:29:30 +08003425 }
Chiachang Wangddb7da62019-06-03 15:50:53 +08003426
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09003427 private void maybeReportCaptivePortalData(@Nullable CaptivePortalDataShim data) {
3428 // Do not clear data even if it is null: access points should not stop serving the API, so
3429 // if the API disappears this is treated as a temporary failure, and previous data should
3430 // remain valid.
3431 if (data == null) return;
3432 try {
3433 data.notifyChanged(mCallback);
3434 } catch (RemoteException e) {
3435 Log.e(TAG, "Error notifying ConnectivityService of new capport data", e);
3436 }
3437 }
3438
Chiachang Wangddb7da62019-06-03 15:50:53 +08003439 /**
3440 * Interface for logging dns results.
3441 */
3442 public interface DnsLogFunc {
3443 /**
3444 * Log function.
3445 */
3446 void log(String s);
3447 }
Chiachang Wange797c9b2019-11-28 14:18:47 +08003448
3449 @Nullable
Chiachang Wangcded6ce2019-12-18 17:27:57 +08003450 private static TcpSocketTracker getTcpSocketTrackerOrNull(Context context, Network network) {
Chiachang Wange797c9b2019-11-28 14:18:47 +08003451 return ((Dependencies.DEFAULT.getDeviceConfigPropertyInt(
3452 NAMESPACE_CONNECTIVITY,
3453 CONFIG_DATA_STALL_EVALUATION_TYPE,
3454 DEFAULT_DATA_STALL_EVALUATION_TYPES)
3455 & DATA_STALL_EVALUATION_TYPE_TCP) != 0)
Remi NGUYEN VAN9eb5c2c2020-05-15 08:21:05 +00003456 ? new TcpSocketTracker(new TcpSocketTracker.Dependencies(context), network)
Chiachang Wange797c9b2019-11-28 14:18:47 +08003457 : null;
3458 }
Chiachang Wang8e232042020-01-17 18:01:59 +08003459
3460 @Nullable
3461 private DnsStallDetector initDnsStallDetectorIfRequired(int type, int threshold) {
3462 return ((type & DATA_STALL_EVALUATION_TYPE_DNS) != 0)
3463 ? new DnsStallDetector(threshold) : null;
3464 }
Remi NGUYEN VAN2fa48c22019-12-09 16:40:02 +09003465
3466 private static Uri getCaptivePortalApiUrl(LinkProperties lp) {
3467 return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp);
3468 }
Remi NGUYEN VAN5daa3702018-12-27 16:43:56 +09003469}