从连接受限看Android网络
现象
谷歌手机,连接受限,经典现象了。
都知道是谷歌网络验证没过,但具体的流程脉络呢?
不如来从现象开始摸一下本质。
摸索
从通知开始
首先找“连接受限”四个字符准没错;
于是发现相关的字符资源在packages/modules/Connectivity/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml中;
<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>
顺藤摸瓜又找到使用这个字符资源的地方,即packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkNotificationManager.java,一个位于Connectivity模块中的通知管理类(官方逐渐将各类组件移出核心包,作为模块包使用);
public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
//...略
if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
&& transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.network_partial_connectivity, name);
details = r.getString(R.string.network_partial_connectivity_detailed);
}
//...略
}
在这个包里搜索,找到使用这个通知管理类的地方,即packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java;
关注NotificationType.PARTIAL_CONNECTIVITY即可;
private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
final String action;
final boolean highPriority;
switch (type) {
//...略
case PARTIAL_CONNECTIVITY:
action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY;
highPriority = nai.networkAgentConfig.explicitlySelected;
break;
}
//...略
mNotifier.showNotification(
nai.network.getNetId(), type, nai, null, pendingIntent, highPriority);
}
接着往上摸,发现是由handlePromptUnvalidated方法来处理是否发出连接受限的通知;
private void handlePromptUnvalidated(Network network) {
if (VDBG || DDBG) log("handlePromptUnvalidated " + network);
NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
if (nai == null || !shouldPromptUnvalidated(nai)) {
return;
}
nai.onPreventAutomaticReconnect();
if (nai.partialConnectivity) {
showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
} else {
showNetworkNotification(nai, NotificationType.NO_INTERNET);
}
}
而且可以明显看到,是否受限是由NetworkAgentInfo的partialConnectivity属性来进行直接判断的——这说明,在此之前,这个属性可能就已经进行了赋值。
是Handler发的通知
先抛开赋值不管,继续看谁在调用handlePromptUnvalidated这个方法;
然后发现有两处,两处都在Handler中;
一处在InternalHandler中,看起来就是会在某个时间点发消息,延迟8秒后去处理不正常的网络;
private class InternalHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//...略
case EVENT_PROMPT_UNVALIDATED: {
handlePromptUnvalidated((Network) msg.obj);
break;
}
//...略
}
}
}
private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
mHandler.sendMessageDelayed(
mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
PROMPT_UNVALIDATED_DELAY_MS);
}
另一处则像是专门的网络状态的监听NetworkStateTrackerHandler,会经由多重判断处理,最终由handleNetworkTested方法调用;
private class NetworkStateTrackerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
if (!maybeHandleNetworkMonitorMessage(msg)
&& !maybeHandleNetworkAgentInfoMessage(msg)) {
maybeHandleNetworkAgentMessage(msg);
}
}
private boolean maybeHandleNetworkMonitorMessage(Message msg) {
final int netId = msg.arg2;
final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
if (nai != null && nai.destroyed) return true;
switch (msg.what) {
//...略
case EVENT_NETWORK_TESTED: {
final NetworkTestedResults results = (NetworkTestedResults) msg.obj;
if (nai == null) break;
handleNetworkTested(nai, results.mTestResult,
(results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
break;
}
//...略
}
return true;
}
//按照源码注释理解,这个方法如果先行测试出网络有问题,会先一步通知并取消Handler的通知
private void handleNetworkTested(
@NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
final boolean wasPartial = nai.partialConnectivity;
nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
//...略
if (!wasPartial && nai.partialConnectivity) {
// Remove delayed message if there is a pending message.
mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
handlePromptUnvalidated(nai.network);
}
//...略
}
}
这里注意到partialConnectivity值很可能经过了一次变动,在这个阶段并不清楚是否为首次赋值操作,并且与之相关的testResult值似乎也只是一个传递下来的结果,只能暂且记下;
看看NetworkStateTrackerHandler
两个Handler中下面这个Handler看着专业一点,就先看看这个吧;
那么主要关注EVENT_NETWORK_TESTED消息是在哪些地方发起的;
然后就会发现是从另一个网络监听NetworkMonitorCallbacks的回调中发起的;
private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
@Override
public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
final Message msg = mTrackerHandler.obtainMessage(
EVENT_NETWORK_TESTED,
0, mNetId,
new NetworkTestedResults(
mNetId, p.result, p.timestampMillis, p.redirectUrl));
mTrackerHandler.sendMessage(msg);
//...略
}
是从notifyNetworkTestedWithExtras方法中发起的;
那么依旧顺藤摸瓜,看这个监听类是从哪里创建的以及相关方法是在什么时候触发的。
创建的地方找到了registerNetworkAgentInternal方法;
private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
int uid) {
final NetworkAgentInfo nai = new NetworkAgentInfo(na,
new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
linkProperties, networkCapabilities,
currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
mQosCallbackTracker, mDeps);
final String extraInfo = networkInfo.getExtraInfo();
final String name = TextUtils.isEmpty(extraInfo)
? nai.networkCapabilities.getSsid() : extraInfo;
if (DBG) log("registerNetworkAgent " + nai);
mDeps.getNetworkStack().makeNetworkMonitor(
nai.network, name, new NetworkMonitorCallbacks(nai));
return nai.network;
}
一看到register字眼就知道可能抓到大鱼了,事实也确实如此;
registerNetworkAgentInternal方法中创建了上文中的关键信息NetworkAgentInfo,并在此创建了网络状态监听类NetworkMonitor及相关回调类NetworkMonitorCallbacks;
而触发notifyNetworkTestedWithExtras的地方则找到了packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java中;
private void notifyNetworkTested(NetworkTestResultParcelable result) {
try {
if (mCallbackVersion <= 5) {
mCallback.notifyNetworkTested(
getLegacyTestResult(result.result, result.probesSucceeded),
result.redirectUrl);
} else {
mCallback.notifyNetworkTestedWithExtras(result);
}
} catch (RemoteException e) {
Log.e(TAG, "Error sending network test result", e);
}
}
这也和上面需要创建NetworkMonitor对应上了,这个方法的参数NetworkTestResultParcelable也可以推断大致就是网络测试的结果信息。
那么现在又有两条分支了;
一条是可以检查NetworkAgentInfo创建后,哪些地方进行了赋值操作,尤其是partialConnectivity属性(不忘初心),因为从构造方法上看是没有这条属性的,说明在其他地方进行了处理;
一条是关注NetworkMonitor的生命周期,以及关注NetworkMonitorCallbacks的相关调用。
先从partialConnectivity属性开始追查,发现除了上文中的ConnectivityService#handleNetworkTested方法中,没有其他地方进行过赋值操作,那么基本可以锁定这个方法;
private void handleNetworkTested(
@NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
final boolean wasPartial = nai.partialConnectivity;
nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
//...略
}
常量值INetworkMonitor中定义了常量值NETWORK_VALIDATION_RESULT_PARTIAL;
const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
也就是说,只要testResult的值为2,就会被判定为连接受限;
而这个方法中的的testResult又是NetworkMonitor和NetworkMonitorCallbacks相关的;
两条分支其实是一条路;
于是目光再次集中到NetworkMonitor这边来。
NetworkMonitor做了什么
那么接着上面的NetworkMonitor#notifyNetworkTested方法继续往上顺,发现了内部类EvaluationState中的reportEvaluationResult方法;
protected class EvaluationState {
protected void reportEvaluationResult(int result, @Nullable String redirectUrl) {
if (mCaptivePortalWantedAsIs) {
result = NETWORK_VALIDATION_RESULT_VALID;
} else if (!isValidationRequired() && mProbeCompleted == 0 && mCallbackVersion >= 11) {
result |= NETWORK_VALIDATION_RESULT_SKIPPED;
}
mEvaluationResult = result;
final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
p.result = result;
p.probesSucceeded = mProbeResults;
p.probesAttempted = mProbeCompleted;
p.redirectUrl = redirectUrl;
p.timestampMillis = SystemClock.elapsedRealtime();
notifyNetworkTested(p);
recordValidationResult(result, redirectUrl);
}
}
这里在创建NetworkTestResultParcelable,但result依旧是传递下来的,同样也受其他值影响;
只能再往上追,追出几个状态类;
public class NetworkMonitor extends StateMachine {
private class DefaultState extends State {
//...略
}
private class ValidatedState extends State {
//...略
}
private class ProbingState extends State {
//...略
}
private class EvaluatingState extends State {
//...略
}
private class EvaluatingPrivateDnsState extends State {
//...略
}
//...略
}
代码过多,不作纠结,只需要知道是状态机就行,毕竟NetworkMonitor直接继承了frameworks/base/core/java/com/android/internal/util/StateMachine,这一段代码中大部分操作都是凭借StateMachine来处理的;
先关注ProbingState,因为其中会有关键常量值NETWORK_VALIDATION_RESULT_PARTIAL出现;
private class ProbingState extends State {
private Thread mThread;
@Override
public void enter() {
//...略
final int token = ++mProbeToken;
final ValidationProperties deps = new ValidationProperties(mNetworkCapabilities);
final URL fallbackUrl = nextFallbackUrl();
final URL[] httpsUrls = Arrays.copyOf(
mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
final URL[] httpUrls = Arrays.copyOf(
mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);
mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
isCaptivePortal(deps, httpsUrls, httpUrls, fallbackUrl))));
mThread.start();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_PROBE_COMPLETE:
if (message.arg1 != mProbeToken) {
return HANDLED;
}
final CaptivePortalProbeResult probeResult =
(CaptivePortalProbeResult) message.obj;
//...略
if (probeResult.isSuccessful()) {
transitionTo(mEvaluatingPrivateDnsState);
} else if (isTermsAndConditionsCaptive(
mInfoShim.getCaptivePortalData(mLinkProperties))) {
//...略
} else if (probeResult.isPortal()) {
//...略
} else if (probeResult.isPartialConnectivity()) {
mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_PARTIAL,
null /* redirectUrl */);
//...略
} else {
//...略
}
return HANDLED;
//...略
}
}
}
可以看到,转换到此状态后(enter),就发了消息CMD_PROBE_COMPLETE,并将结果由isCaptivePortal方法封装塞进了message;
因此问题变成了怎样去判断probeResult.isPartialConnectivity();
先找到这个类的所在packages/modules/NetworkStack/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java,看看相关的方法和属性;
public class CaptivePortalProbeResult {
public static final int PARTIAL_CODE = -1;
public boolean isPartialConnectivity() {
return mHttpResponseCode == PARTIAL_CODE;
}
}
也就是CaptivePortalProbeResult的mHttpResponseCode只要被赋值为-1,probeResult.isPartialConnectivity()则为true。
到这里又可以通过两方面来摸索:
一,状态机的启动顺序,什么时候转换到ProbingState;
二,CaptivePortalProbeResult是依据什么来进行创建赋值的。
NetworkMonitor是一个状态机
是一个状态机无疑,可以从其构方法中看出有哪些状态可供转换;
addState(mDefaultState);
addState(mMaybeNotifyState, mDefaultState);
addState(mEvaluatingState, mMaybeNotifyState);
addState(mProbingState, mEvaluatingState);
addState(mWaitingForNextProbeState, mEvaluatingState);
addState(mCaptivePortalState, mMaybeNotifyState);
addState(mEvaluatingPrivateDnsState, mDefaultState);
addState(mEvaluatingBandwidthState, mDefaultState);
addState(mValidatedState, mDefaultState);
setInitialState(mDefaultState);
默认状态或者初始状态正是DefaultState;
public void notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
final NetworkMonitorParameters params = new NetworkMonitorParameters();
params.linkProperties = lp;
params.networkCapabilities = nc;
notifyNetworkConnectedParcel(params);
}
public void notifyNetworkConnectedParcel(NetworkMonitorParameters params) {
sendMessage(CMD_NETWORK_CONNECTED, params);
}
private class DefaultState extends State {
@Override
public void enter() {
mContext.registerReceiver(mConfigurationReceiver,
new IntentFilter(ACTION_CONFIGURATION_CHANGED));
checkAndRenewResourceConfig();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_NETWORK_CONNECTED:
updateConnectedNetworkAttributes(message);
logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
transitionTo(mEvaluatingState);
return HANDLED;
}
}
checkAndRenewResourceConfig方法关系到下文中一些重要参数;
而ProbingState是转换EvaluatingState后随即进入的;
private class EvaluatingState extends State {
private Uri mEvaluatingCapportUrl;
@Override
public void enter() {
//...略
sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
//...略
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_REEVALUATE:
//...略
transitionTo(mProbingState);
return HANDLED;
}
}
EvaluatingState又是接收到CMD_NETWORK_CONNECTED消息后进入的;
那么很明显,当网络连接成功后,就会进入EvaluatingState;
也就是连接成功后,就会进行评估和嗅探操作;
CaptivePortalProbeResult从何而来
再来关注可以得到嗅探结果的isCaptivePortal方法;
private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
//...略
final CaptivePortalProbeResult result;
if (pacUrl != null) {
result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
} else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
httpsUrls[0], httpUrls[0], fallbackUrl);
} else if (mUseHttps) {
result = sendMultiParallelHttpAndHttpsProbes(properties, proxyInfo, httpsUrls,
httpUrls);
} else {
result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
}
//...略
return result;
}
看着一大串if,来一个个排除;
pacUrl,大概理解为代理地址,默认为空;
//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
if (pacUrl == null) {
return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
}
}
//构造方法,LinkProperties创建时为无参构造
public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
IpConnectivityLog logger, SharedLog validationLogs,
@NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
@Nullable TcpSocketTracker tst) {
//...略
mLinkProperties = new LinkProperties();
}
//packages/modules/Connectivity/framework/src/android/net/LinkProperties.java
public LinkProperties() {
mParcelSensitiveFields = false;
}
public LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) {
//...略
mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy);
//...略
}
public @Nullable ProxyInfo getHttpProxy() {
return mHttpProxy;
}
mUseHttps,大概理解为是否使用https方式,默认为true;
//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
IpConnectivityLog logger, SharedLog validationLogs,
@NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
@Nullable TcpSocketTracker tst) {
//...略
mUseHttps = getUseHttpsValidation();
}
private boolean getUseHttpsValidation() {
return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
}
httpsUrls和httpUrls,是传递进来的参数,往回退一下,看从哪里取值;
final URL[] httpsUrls = Arrays.copyOf(
mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
final URL[] httpUrls = Arrays.copyOf(
mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);
发现在checkAndRenewResourceConfig方法中有过赋值,与上文中的DefaultState呼应;
private boolean checkAndRenewResourceConfig() {
//...略
final URL[] captivePortalHttpsUrls = makeCaptivePortalHttpsUrls(customizedContext);
if (!Arrays.equals(mCaptivePortalHttpsUrls, captivePortalHttpsUrls)) {
mCaptivePortalHttpsUrls = captivePortalHttpsUrls;
reevaluationNeeded = true;
log("checkAndRenewResourceConfig: update captive portal https urls to "
+ Arrays.toString(mCaptivePortalHttpsUrls));
}
final URL[] captivePortalHttpUrls = makeCaptivePortalHttpUrls(customizedContext);
if (!Arrays.equals(mCaptivePortalHttpUrls, captivePortalHttpUrls)) {
mCaptivePortalHttpUrls = captivePortalHttpUrls;
reevaluationNeeded = true;
log("checkAndRenewResourceConfig: update captive portal http urls to "
+ Arrays.toString(mCaptivePortalHttpUrls));
}
//...略
}
这里又直接抄了captivePortalHttpsUrls和mCaptivePortalHttpUrls的值;
private URL[] makeCaptivePortalHttpsUrls(@NonNull Context context) {
final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
if (testUrl != null) return new URL[] { testUrl };
final String firstUrl = getCaptivePortalServerHttpsUrl(context);
try {
final URL[] settingProviderUrls =
combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTPS_URLS);
// firstUrl will at least be default configuration, so default value in
// getProbeUrlArrayConfig is actually never used.
return getProbeUrlArrayConfig(context, settingProviderUrls,
R.array.config_captive_portal_https_urls,
DEFAULT_CAPTIVE_PORTAL_HTTPS_URLS, this::makeURL);
} catch (Exception e) {
// Don't let a misconfiguration bootloop the system.
Log.e(TAG, "Error parsing configured https URLs", e);
// Ensure URL aligned with legacy configuration.
return new URL[]{makeURL(firstUrl)};
}
}
private String getCaptivePortalServerHttpsUrl(@NonNull Context context) {
return getSettingFromResource(context,
R.string.config_captive_portal_https_url, mCaptivePortalHttpsUrlFromSetting,
context.getResources().getString(
R.string.default_captive_portal_https_url));
}
private URL[] makeCaptivePortalHttpUrls(@NonNull Context context) {
final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
if (testUrl != null) return new URL[] { testUrl };
final String firstUrl = getCaptivePortalServerHttpUrl(context);
try {
final URL[] settingProviderUrls =
combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTP_URLS);
// firstUrl will at least be default configuration, so default value in
// getProbeUrlArrayConfig is actually never used.
return getProbeUrlArrayConfig(context, settingProviderUrls,
R.array.config_captive_portal_http_urls,
DEFAULT_CAPTIVE_PORTAL_HTTP_URLS, this::makeURL);
} catch (Exception e) {
// Don't let a misconfiguration bootloop the system.
Log.e(TAG, "Error parsing configured http URLs", e);
// Ensure URL aligned with legacy configuration.
return new URL[]{makeURL(firstUrl)};
}
}
public String getCaptivePortalServerHttpUrl(@NonNull Context context) {
return getSettingFromResource(context,
R.string.config_captive_portal_http_url, mCaptivePortalHttpUrlFromSetting,
context.getResources().getString(
R.string.default_captive_portal_http_url));
}
最终又是在资源文件中取值,而且源码中的注释也说得很清楚,url[]应当只有firstUrl一个值,因此只要default_captive_portal_http_url和default_captive_portal_https_url的值不为空,那么整个url[]确实就只有一个值;
连接受限的直接原因
在packages/modules/NetworkStack/res/values/config.xml中;
<string name="config_captive_portal_https_url" translatable="false"></string>
<string name="default_captive_portal_https_url" translatable="false">https://www.google.com/generate_204</string>
<string name="config_captive_portal_http_url" translatable="false"></string>
<string name="default_captive_portal_http_url" translatable="false">http://connectivitycheck.gstatic.com/generate_204</string>
有值,两个值的长度恰好为1,mUseHttps又为true,于是满足条件:
private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
//...略
if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
httpsUrls[0], httpUrls[0], fallbackUrl);
}
return result;
}
到这里,已经初步证实了最初的猜想,https://www.google.com/generate_204这个验证地址,国内肯定连接不上,因此被判定为连接受限;
也就是如果能改变default_captive_portal_https_url的值,连接受限4个字将不再出现。
嗅探是怎样进行的
嗅探的结果才是最终的判断依据,那么了解嗅探是怎样进行的也是有必要的了;
先看sendHttpAndHttpsParallelWithFallbackProbes方法;
private static final int PROBE_TIMEOUT_MS = 3000;
private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
ValidationProperties properties, ProxyInfo proxy, URL httpsUrl, URL httpUrl,
URL fallbackUrl) {
// Number of probes to wait for. If a probe completes with a conclusive answer
// it shortcuts the latch immediately by forcing the count to 0.
final CountDownLatch latch = new CountDownLatch(2);
final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
final ProbeThread httpsProbe = new ProbeThread(latch, properties, proxy, httpsUrl,
ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
final ProbeThread httpProbe = new ProbeThread(latch, properties, proxy, httpUrl,
ValidationProbeEvent.PROBE_HTTP, capportApiUrl);
try {
httpsProbe.start();
httpProbe.start();
latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
validationLog("Error: probes wait interrupted!");
return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
}
final CaptivePortalProbeResult httpsResult = httpsProbe.result();
final CaptivePortalProbeResult httpResult = httpProbe.result();
if (isConclusiveResult(httpResult, capportApiUrl)) {
reportProbeResult(httpProbe.result());
return httpResult;
}
if (isConclusiveResult(httpsResult, capportApiUrl)) {
reportProbeResult(httpsProbe.result());
return httpsResult;
}
//...略
try {
httpProbe.join();
reportProbeResult(httpProbe.result());
if (httpProbe.result().isPortal()) {
return httpProbe.result();
}
httpsProbe.join();
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());
if (httpsProbe.result().isFailed() && httpProbe.result().isSuccessful()) {
return CaptivePortalProbeResult.PARTIAL;
}
return httpsProbe.result();
} catch (InterruptedException e) {
validationLog("Error: http or https probe wait interrupted!");
return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
}
}
代码较多,但逻辑比较清晰,就是通过httpProbe和httpsProbe两个探针同时进行嗅探,如果3秒内不能得到确切结果,就先后嗅探http和https的结果,http有问题就直接返回结果;
而如果http成功https失败,就返回PARTIAL结果,也就是连接受限;
这里要结合CaptivePortalProbeResult类的一些属性和方法看才行:
public class CaptivePortalProbeResult {
public static final int SUCCESS_CODE = 204;
public static final int FAILED_CODE = 599;
public static final int PORTAL_CODE = 302;
public static final int PARTIAL_CODE = -1;
public static final CaptivePortalProbeResult PRIVATE_IP =
new CaptivePortalProbeResult(DNS_PRIVATE_IP_RESPONSE_CODE, 1 << PROBE_HTTP);
// Partial connectivity should be concluded from both HTTP and HTTPS probes.
@NonNull
public static final CaptivePortalProbeResult PARTIAL = new CaptivePortalProbeResult(
PARTIAL_CODE, 1 << PROBE_HTTP | 1 << PROBE_HTTPS);
@ProbeType
public final int probeType;
@IntDef(value = {
PROBE_UNKNOWN,
1 << PROBE_HTTP,
1 << PROBE_HTTPS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProbeType {
}
//...略
public boolean isSuccessful() {
return isSuccessCode(mHttpResponseCode);
}
public boolean isPortal() {
return isPortalCode(mHttpResponseCode);
}
private static boolean isSuccessCode(int responseCode) {
return responseCode == SUCCESS_CODE;
}
public static boolean isPortalCode(int responseCode) {
return !isSuccessCode(responseCode) && (responseCode >= 200) && (responseCode <= 399);
}
public boolean isFailed() {
return !isSuccessful() && !isPortal();
}
public boolean isPartialConnectivity() {
return mHttpResponseCode == PARTIAL_CODE;
}
结合着响应码来看,如果嗅探或请求成功了,响应码会是204,即无内容的响应码,其他2XX或3XX代表至少地址有所反应,除此之外则为失败。
ProbeThread
上面可以看出探网操作被封装为一个可执行的线程类:
private class ProbeThread extends Thread {
//...略
private volatile CaptivePortalProbeResult mResult;
public CaptivePortalProbeResult result() {
return mResult;
}
@Override
public void run() {
mResult = mProbe.sendProbe();
if (isConclusiveResult(mResult, mProbe.mCaptivePortalApiUrl)) {
// Stop waiting immediately if any probe is conclusive.
while (mLatch.getCount() > 0) {
mLatch.countDown();
}
}
// Signal this probe has completed.
mLatch.countDown();
}
}
再拿其中一个具体的probe来看嗅探的具体内容,就HttpsProbe 吧;
final class HttpsProbe extends Probe {
HttpsProbe(ValidationProperties properties, ProxyInfo proxy, URL url,
Uri captivePortalApiUrl) {
super(properties, proxy, url, captivePortalApiUrl);
}
@Override
protected CaptivePortalProbeResult sendProbe() {
return sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTPS);
}
}
接下来会看到具体的操作,先在sendDnsAndHttpProbes方法中解析地址,再在makeProbeConnection方法中创建连接,最后由sendHttpProbe发送具体的请求并接收结果;
private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
final String host = (proxy != null) ? proxy.getHost() : url.getHost();
final InetAddress[] resolvedAddr = sendDnsProbe(host);
if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
&& (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
return CaptivePortalProbeResult.PRIVATE_IP;
}
return sendHttpProbe(url, probeType, null);
}
protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
@Nullable CaptivePortalProbeSpec probeSpec) {
HttpURLConnection urlConnection = null;
int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
String redirectUrl = null;
final Stopwatch probeTimer = new Stopwatch().start();
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
NetworkStackConstants.TAG_SYSTEM_PROBE);
try {
urlConnection = makeProbeConnection(url, followRedirect);
String requestHeader = urlConnection.getRequestProperties().toString();
httpResponseCode = urlConnection.getResponseCode();
redirectUrl = urlConnection.getHeaderField("location");
//...略
} catch (IOException e) {
validationLog(probeType, url, "Probe failed with exception " + e);
if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
// TODO: Ping gateway and DNS server and log results.
}
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
TrafficStats.setThreadStatsTag(oldTag);
}
//...略
return probeResult;
}
private HttpURLConnection makeProbeConnection(URL url, boolean followRedirects)
throws IOException {
final HttpURLConnection conn = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
conn.setInstanceFollowRedirects(followRedirects);
conn.setConnectTimeout(SOCKET_TIMEOUT_MS);
conn.setReadTimeout(SOCKET_TIMEOUT_MS);
conn.setRequestProperty("Connection", "close");
conn.setUseCaches(false);
if (mCaptivePortalUserAgent != null) {
conn.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
}
return conn;
}
回过头看看InternalHander
不要忘记以上都是从NetworkStateTrackerHandler中追踪而得,另一个InternalHander是也可以触发通知的;
现在再回过头看看InternalHander中会如何处理,也就是看谁调用了scheduleUnvalidatedPrompt方法,这个方法的作用是8秒后就向InternalHander发送消息通知当前的网络存在故障;
于是就在当前ConnectivityService中找到updateNetworkInfo方法;
private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) {
//...略
if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
networkAgent.everConnected = true;
networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);
handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
null);
//...略
if (networkAgent.networkAgentConfig.acceptPartialConnectivity) {
networkAgent.networkMonitor().setAcceptPartialConnectivity();
}
//...略
scheduleUnvalidatedPrompt(networkAgent);
//...略
} else if (state == NetworkInfo.State.DISCONNECTED) {
//...略
} else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
state == NetworkInfo.State.SUSPENDED)) {
//...略
}
}
这方法一看调用的地方就不会只有一处;
果然找到3处;
但最值得关注无疑是下面的handleRegisterNetworkAgent方法:
private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
final LinkProperties lp = new LinkProperties(nai.linkProperties);
processCapabilitiesFromAgent(nai, nc);
nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc));
processLinkPropertiesFromAgent(nai, lp);
nai.linkProperties = lp;
nai.onNetworkMonitorCreated(networkMonitor);
mNetworkAgentInfos.add(nai);
synchronized (mNetworkForNetId) {
mNetworkForNetId.put(nai.network.getNetId(), nai);
}
try {
networkMonitor.start();
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
nai.notifyRegistered();
NetworkInfo networkInfo = nai.networkInfo;
updateNetworkInfo(nai, networkInfo);
updateVpnUids(nai, null, nai.networkCapabilities);
}
在这里启动networkMonitor,似乎又和上文中某些节点对应上了;
并且我们记得NetworkAgentInfo和INetworkMonitor正好是在上文中另一个Handler分析中的registerNetworkAgentInternal方法中创建的:
private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
int uid) {
final NetworkAgentInfo nai = new NetworkAgentInfo(na,
new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
linkProperties, networkCapabilities,
currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
mQosCallbackTracker, mDeps);
mDeps.getNetworkStack().makeNetworkMonitor(
nai.network, name, new NetworkMonitorCallbacks(nai));
return nai.network;
}
来看看是谁把这里创建的两种信息传递到InternalHandler并交由handleRegisterNetworkAgent方法的;
private class InternalHandler extends Handler {
public InternalHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_REGISTER_NETWORK_AGENT: {
final Pair<NetworkAgentInfo, INetworkMonitor> arg =
(Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
handleRegisterNetworkAgent(arg.first, arg.second);
break;
}
}
}
}
然后惊喜地发现,是老熟人了;
private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
@Override
public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
new Pair<>(mNai.getAndDestroy(), networkMonitor)));
}
}
当然这里的回调是进程间的调用,关系到packages/modules/NetworkStack/src/com/android/server/NetworkStackService.java;
public class NetworkStackService extends Service {
@Override
public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
throws RemoteException {
mPermChecker.enforceNetworkStackCallingPermission();
updateNetworkStackAidlVersion(cb.getInterfaceVersion(), cb.getInterfaceHash());
final SharedLog log = addValidationLogs(network, name);
final NetworkMonitor nm = mDeps.makeNetworkMonitor(mContext, cb, network, log, this);
cb.onNetworkMonitorCreated(new NetworkMonitorConnector(nm, mPermChecker));
}
}
于是大致的脉络就有了——
registerNetworkAgentInternal方法中创建了NetworkAgentInfo和INetworkMonitor,并且由于调用了makeNetworkMonitor方法以及NetworkMonitorCallbacks回调存在的原因,NetworkMonitor创建时就回调了onNetworkMonitorCreated方法,随之向InternalHandler发送消息,调用了handleRegisterNetworkAgent方法。
那么接下来的问题就是,registerNetworkAgentInternal是干什么的,什么时候会调用呢?
registerNetworkAgentInternal的来由
往上追溯,还在ConnectivityService中,但却罕见的是一个公有方法了,意味着这可能是一个入口;
public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
@NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
int providerId) {
//...略
final int uid = mDeps.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
return registerNetworkAgentInternal(na, networkInfo, linkProperties,
networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
} finally {
Binder.restoreCallingIdentity(token);
}
}
它也确实是一个入口,毕竟ConnectivityService本身就太眼熟了;
在packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java中找到了;
public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
int providerId) {
try {
return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
ConnectivityManager就更熟悉了;
不过调用这个方法的地方却极少,只在packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java中出现;
public abstract class NetworkAgent {
public Network register() {
if (VDBG) log("Registering NetworkAgent");
synchronized (mRegisterLock) {
if (mNetwork != null) {
throw new IllegalStateException("Agent already registered");
}
final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
.getSystemService(Context.CONNECTIVITY_SERVICE);
mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
new NetworkInfo(mInitialConfiguration.info),
mInitialConfiguration.properties, mInitialConfiguration.capabilities,
mInitialConfiguration.score, mInitialConfiguration.config, providerId);
mInitialConfiguration = null; // All this memory can now be GC'd
}
return mNetwork;
}
}
NetworkAgent的作用
NetworkAgent,网络代理,注册;
既然是抽象类,那么就肯定有具体的实现类;
如果只讨论wifi的话,就是packages/modules/Wifi/service/java/com/android/server/wifi/WifiNetworkAgent.java;
public class WifiNetworkAgent extends NetworkAgent {
public WifiNetworkAgent(
@NonNull Context context,
@NonNull Looper looper,
@NonNull NetworkCapabilities nc,
@NonNull LinkProperties lp,
@NonNull NetworkAgentConfig config,
@Nullable NetworkProvider provider,
@NonNull Callback wifiNetworkAgentCallback) {
super(context, looper, TAG, nc, lp, ConnectedScore.WIFI_INITIAL_SCORE, config, provider);
mCurrentNetworkCapabilities = nc;
mCallback = wifiNetworkAgentCallback;
register();
}
}
构造方法中就有register方法;
哪里有相关应用呢?
比如packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java中;
public class ClientModeImpl extends StateMachine implements ClientMode {
class L2ConnectedState extends State {
@Override
public void enter() {
//...略
mNetworkAgent = mWifiInjector.makeWifiNetworkAgent(nc, mLinkProperties, naConfig,
mNetworkFactory.getProvider(), new WifiNetworkAgentCallback());
//...略
}
//...略
}
}
ClientModeImpl也是一个状态机,根据其构造方法中添加的各类状态来看,它关系着wifi的连接以及相关配置;
addState(mConnectableState); {
addState(mConnectingOrConnectedState, mConnectableState); {
addState(mL2ConnectingState, mConnectingOrConnectedState);
addState(mL2ConnectedState, mConnectingOrConnectedState); {
addState(mL3ProvisioningState, mL2ConnectedState);
addState(mL3ConnectedState, mL2ConnectedState);
addState(mRoamingState, mL2ConnectedState);
}
}
addState(mDisconnectedState, mConnectableState);
}
setInitialState(mDisconnectedState);
具体的状态转换不再赘述,理解大致脉络即可。
总结时序
照例来张时序图梳理一下;
Frameworks里的东西就这样,牵扯太广,凑合着看,其实只要有个模糊的概念就行;
主要是要习惯并掌握追溯流程的方法。
以上。