前情提要
手上正在开发的Android设备,连接公司某个测试的小米路由器会提示“网络无法联通”,其他路由器和手机热点则正常。
由经验可知,该情况大概率由于“Great Wall”影响了wifi模块相关服务的原生逻辑(检测网络联通),继而推测和dns以及connectivityService相关。
DNS地址对比
tracyliu@tracyliudeMacBook-Pro ~ % adb shell getprop | grep dns
[net.dns1]: [192.168.31.1]
上面连接xiaomi路由器,下边是手机热点
[net.dns1]: [fe80::40c7:11ff:fe65:3b64%wlan0]
[net.dns2]: [172.20.10.1]
路由器dns地址解析逻辑
如今多数无线路由器采用了一种 DHCP 的技术。默认情况下,在为路由器设置好 DNS 后,路由器会将连接到该网络上的设备的 DNS 服务器,设置为自己的 IP 地址。网络上设备的 DNS 请求,统一发送至路由器 IP 地址,此时路由器扮演各设备的 DNS 服务器。然后,路由器转发 DNS 请求,到实际的 DNS 服务器。实际的 DNS 服务器解析域名 IP,返回给路由器。最后,路由器再把 IP 返回给终端设备。
如果设备没有单独设置DNS服务器,默认「自动从 DHCP 获取 DNS」,那么连接路由器后,路由器的ip地址就会被设置为设备的dns服务器地址。192.168.31.1。路由器上实际的dns服务器地址则是手动配置或者是运营商提供的DNS服务器地址。
不同的dns服务器地址,对connectivitycheck.gstatic.com/generate_20…
log分析
07-04 19:24:38.868 991 2825 E liudi : sendDnsAndHttpProbes url = https://connectivitycheck.gstatic.com/generate_204 probeType = 2 resolvedAddr = [/14.215.177.38]
上面是xiaomi路由器解析的地址,下边是手机热点解析的地址。前者指向百度,后者是谷歌云。
07-04 19:28:26.343 981 2857 E liudi : sendDnsAndHttpProbes url = https://connectivitycheck.gstatic.com/generate_204 probeType = 2 resolvedAddr = [/203.208.50.34]
当设备连接小米路由器时会发生异常 SSLPeerUnverifiedException
并且打印出证书信息DN: CN=baidu.com,O=Beijing Baidu Netcom Science Technology 即在访问该连接时,验证百度证书的合法性没通过。
PROBE_HTTPS https://connectivitycheck.gstatic.com/generate_204 Probe failed with exception javax.net.ssl.SSLPeerUnverifiedException: Hostname connectivitycheck.gstatic.com not verified:
07-05 17:07:10.683 986 5490 D NetworkMonitor/100: PROBE_DNS www.googleapis.cn 30ms OK 220.181.174.162
07-05 17:07:10.688 986 5489 D NetworkMonitor/100: PROBE_DNS connectivitycheck.gstatic.com 35ms OK 14.215.177.38
07-05 17:07:10.759 986 5490 D NetworkMonitor/100: PROBE_HTTP http://www.googleapis.cn/generate_204 time=74ms ret=204 request={Connection=[close], User-Agent=[Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.32 Safari/537.36]} headers={null=[HTTP/1.1 204 No Content], Connection=[close], Content-Length=[0], Date=[Tue, 05 Jul 2022 09:07:11 GMT], X-Android-Received-Millis=[1657012030758], X-Android-Response-Source=[NETWORK 204], X-Android-Selected-Protocol=[http/1.1], X-Android-Sent-Millis=[1657012030703]}
07-05 17:07:10.859 986 5489 D NetworkMonitor/100: PROBE_HTTPS https://connectivitycheck.gstatic.com/generate_204 Probe failed with exception javax.net.ssl.SSLPeerUnverifiedException: Hostname connectivitycheck.gstatic.com not verified:
07-05 17:07:10.859 986 5489 D NetworkMonitor/100: certificate: sha1/BTqK49yeWq7VXxhIKM4VVD7PHxo=
07-05 17:07:10.859 986 5489 D NetworkMonitor/100: DN: CN=baidu.com,O=Beijing Baidu Netcom Science Technology Co.\, Ltd,OU=service operation department,L=beijing,ST=beijing,C=CN
07-05 17:07:10.859 986 5489 D NetworkMonitor/100: subjectAltNames: [baidu.com, click.hm.baidu.com, cm.pos.baidu.com, log.hm.baidu.com, update.pan.baidu.com, wn.pos.baidu.com, *.91.com, *.aipage.cn, *.aipage.com, *.apollo.auto, *.baidu.com, *.baidubce.com, *.baiducontent.com, *.baidupcs.com, *.baidustatic.com, *.baifubao.com, *.bce.baidu.com, *.bcehost.com, *.bdimg.com, *.bdstatic.com, *.bdtjrcv.com, *.bj.baidubce.com, *.chuanke.com, *.cloud.baidu.com, *.dlnel.com, *.dlnel.org, *.dueros.baidu.com, *.eyun.baidu.com, *.fanyi.baidu.com, *.gz.baidubce.com, *.hao123.baidu.com, *.hao123.com, *.hao222.com, *.haokan.com, *.im.baidu.com, *.map.baidu.com, *.mbd.baidu.com, *.mipcdn.com, *.news.baidu.com, *.nuomi.com, *.pae.baidu.com, *.safe.baidu.com, *.smartapps.cn, *.su.baidu.com, *.trustgo.com, *.vd.bdstatic.com, *.xueshu.baidu.com, apollo.auto, baifubao.com, dwz.cn, mct.y.nuomi.com, www.baidu.cn, www.baidu.com.cn]
07-05 17:07:10.967 986 5488 D NetworkMonitor/100: PROBE_FALLBACK http://developers.google.cn/generate_204 time=105ms ret=204 request={Connection=[close], User-Agent=[Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.32 Safari/537.36]} headers={null=[HTTP/1.1 204 No Content], Connection=[close], Content-Length=[0], Date=[Tue, 05 Jul 2022 09:07:11 GMT], X-Android-Received-Millis=[1657012030966], X-Android-Response-Source=[NETWORK 204], X-Android-Selected-Protocol=[http/1.1], X-Android-Sent-Millis=[1657012030905]}
07-05 17:07:10.968 986 5488 D NetworkMonitor/100: isCaptivePortal: isSuccessful()=false isPortal()=false RedirectUrl=null isPartialConnectivity()=true Time=318ms
系统保存wifi信息文件位置
/data/misc/apexdata/com.android.wifi/WifiConfigStore.xml
代码流程
整个过程主要涉及ConnectivityService和NetWorkMonitor两个类。大概逻辑每次有网络连接的时候都会从ConnectivityManager开始regesiterNetworkAgent,从而创建一个NetworkMonitor去检测网络。在NetworkMonitor的构造方法中会add一系列的状态机处理逻辑。
NetworkMonitor主要是检测网络有效性的,通过Http封装类去ping一个网站,根据ping网站的结果来影响评分值。EvaluatingState 评估状态,ProbingState探测状态,这里重点看ProbingState
private class ProbingState extends State {
private Thread mThread;
@Override
public void enter() {
mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
isCaptivePortal(deps))));
mThread.start();
}
接下来是两条支线
- CMD_PROBE_COMPLETE
- isCaptivePortal(deps)
CMD_PROBE_COMPLETE 探测过程
probeResult的结果是isCaptivePortal(deps)返回的,当连接正常wifi是时probeResult.isSuccessful() = true,当连接有问题的小米wifi时probeResult.isPartialConnectivity() = true。
贴log
case CMD_PROBE_COMPLETE:
final CaptivePortalProbeResult probeResult =
(CaptivePortalProbeResult) message.obj;
mLastProbeTime = SystemClock.elapsedRealtime();
maybeWriteDataStallStats(probeResult);
if (probeResult.isSuccessful()) {
// Transit EvaluatingPrivateDnsState to get to Validated
// state (even if no Private DNS validation required).
transitionTo(mEvaluatingPrivateDnsState);
} else if (probeResult.isPortal()) {
mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
probeResult.redirectUrl);
mLastPortalProbeResult = probeResult;
transitionTo(mCaptivePortalState);
} else if (probeResult.isPartialConnectivity()) {
mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_PARTIAL,
null /* redirectUrl */);
maybeDisableHttpsProbing(mAcceptPartialConnectivity);
if (mAcceptPartialConnectivity) {
transitionTo(mEvaluatingPrivateDnsState);
} else {
transitionTo(mWaitingForNextProbeState);
}
} else {
logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
null /* redirectUrl */);
transitionTo(mWaitingForNextProbeState);
}
return HANDLED;
继续跟代码
mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,probeResult.redirectUrl)
reportEvaluationResult()
notifyNetworkTested(p);
终于来到ConnectivityService,EVENT_NETWORK_TESTED这个msg中。
frameworks/base/services/core/java/com/android/server/ConnectivityService.java
@Override
public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
final Message m = mConnectivityDiagnosticsHandler.obtainMessage(
ConnectivityDiagnosticsHandler.EVENT_NETWORK_TESTED,
new ConnectivityReportEvent(p.timestampMillis, nai));
mConnectivityDiagnosticsHandler.sendMessage(m);
}
case EVENT_NETWORK_TESTED: {
final NetworkTestedResults results = (NetworkTestedResults) msg.obj;
final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(results.mNetId);
if (nai == null) break;
handleNetworkTested(nai, results.mTestResult,
(results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
break;
}
重点看handleNetworkTested,这里涉及到两种思路
- 如果判断是wifi连接状态 重新评估一下valid状态
- 跟踪代码,分析是那里最终设置阻止重联
当valid = true时,会走到NetworkAgent.CMD_REPORT_NETWORK_STATUS中,把对应wifi的status最终写入wificonfig.xml。 当连接小米wifi的时候则会最终走到handlePromptUnvalidated()判断中。
以上最终写入的逻辑都在WifiNetWorkAgent中,判断并通过wificonfigManager写入文件。具体的逻辑有点不完美,理论上valid = true 和 nai.partialConnectivity = true不应该同时出现。
private void handleNetworkTested(
@NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
final boolean wasPartial = nai.partialConnectivity;
nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
final boolean partialConnectivityChanged =
(wasPartial != nai.partialConnectivity);
boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0);
final boolean wasValidated = nai.lastValidated;
final boolean wasDefault = isDefaultNetwork(nai);
NetworkInfo info = new NetworkInfo(nai.networkInfo);
/** if (!valid && info.getType() == ConnectivityManager.TYPE_WIFI
&& isNetworkReachableActually(nai)) {
valid = true;
log("isNetworkReachableActually: " + valid);
}
**/
......
updateInetCondition(nai);
// Let the NetworkAgent know the state of its network
Bundle redirectUrlBundle = new Bundle();
redirectUrlBundle.putString(NetworkAgent.REDIRECT_URL_KEY, redirectUrl);
// TODO: Evaluate to update partial connectivity to status to NetworkAgent.
if (!valid && nai.everValidated && TextUtils.isEmpty(redirectUrl)) {
log("not send result of INVALID_NETWORK in such condition");
} else {
nai.asyncChannel.sendMessage(
NetworkAgent.CMD_REPORT_NETWORK_STATUS,
(valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK),
0, redirectUrlBundle);
}
// If NetworkMonitor detects partial connectivity before
// EVENT_PROMPT_UNVALIDATED arrives, show the partial connectivity notification
// immediately. Re-notify partial connectivity silently if no internet
// notification already there.
if (!wasPartial && nai.partialConnectivity) {
// Remove delayed message if there is a pending message.
mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
handlePromptUnvalidated(nai.network);
}
if (wasValidated && !nai.lastValidated) {
handleNetworkUnvalidated(nai);
}
}
这里直接注释掉nai.asyncChannel.sendMessage(NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT);
private void handlePromptUnvalidated(Network network) {
if (DBG) log("handlePromptUnvalidated " + network);
NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
if (nai == null || !shouldPromptUnvalidated(nai)) {
return;
}
// Stop automatically reconnecting to this network in the future. Automatically connecting
// to a network that provides no or limited connectivity is not useful, because the user
// cannot use that network except through the notification shown by this method, and the
// notification is only shown if the network is explicitly selected by the user.
if (nai.everValidated) {
log("handlePromptUnvalidated: not send CMD_PREVENT_AUTOMATIC_RECONNECT in such condition");
} else {
log("handlePromptUnvalidated: send CMD_PREVENT_AUTOMATIC_RECONNECT ");
nai.asyncChannel.sendMessage(NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT);
}
}
isCaptivePortal(deps)
接下来具体看isCaptivePortal过程,正常会走到sendHttpAndHttpsParallelWithFallbackProbes这里。
private CaptivePortalProbeResult isCaptivePortal(EvaluationThreadDeps deps) {
.......
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) {
// Probe results are reported inside sendHttpAndHttpsParallelWithFallbackProbes.
result = sendHttpAndHttpsParallelWithFallbackProbes(deps, proxyInfo,
httpsUrls[0], httpUrls[0]);
// 正常会走到这里
} else if (mUseHttps) {
// Support result aggregation from multiple Urls.
result = sendMultiParallelHttpAndHttpsProbes(deps, proxyInfo, httpsUrls, httpUrls);
} else {
result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
}
........
return result;
下面可看到有httpsProbe 和 httpProbe两个 ProbeThread区别在于url不同,通过以下代码可知,当https探测失败,http探测成功时,CaptivePortalProbeResult.PARTIAL。
public static final CaptivePortalProbeResult PARTIAL = new CaptivePortalProbeResult(
PARTIAL_CODE, 1 << PROBE_HTTP | 1 << PROBE_HTTPS);
public boolean isPartialConnectivity() {
return mHttpResponseCode == PARTIAL_CODE;
}
这里呼应上https探测失败时的逻辑。
private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
EvaluationThreadDeps deps, ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
// 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, deps, proxy, httpsUrl,
ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
final ProbeThread httpProbe = new ProbeThread(latch, deps, 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();
// Look for a conclusive probe result first.
if (isConclusiveResult(httpResult, capportApiUrl)) {
reportProbeResult(httpProbe.result());
return httpResult;
}
if (isConclusiveResult(httpsResult, capportApiUrl)) {
reportProbeResult(httpsProbe.result());
return httpsResult;
}
// Otherwise wait until http and https probes completes and use their results.
try {
httpProbe.join();
reportProbeResult(httpProbe.result());
if (httpProbe.result().isPortal()) {
return httpProbe.result();
}
httpsProbe.join();
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());
final boolean isHttpSuccessful =
(httpProbe.result().isSuccessful()
|| (fallbackProbeResult != null && fallbackProbeResult.isSuccessful()));
if (httpsProbe.result().isFailed() && isHttpSuccessful) {
Log.e("liudi","Bingo");
return CaptivePortalProbeResult.PARTIAL;
}
return httpsProbe.result();
} catch (InterruptedException e) {
validationLog("Error: http or https probe wait interrupted!");
return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
}
}
这里继续看下为什么https探测失败
private class ProbeThread extends Thread {
private final CountDownLatch mLatch;
private final Probe mProbe;
ProbeThread(CountDownLatch latch, EvaluationThreadDeps deps, ProxyInfo proxy, URL url,
int probeType, Uri captivePortalApiUrl) {
mLatch = latch;
mProbe = (probeType == ValidationProbeEvent.PROBE_HTTPS)
? new HttpsProbe(deps, proxy, url, captivePortalApiUrl)
: new HttpProbe(deps, proxy, url, captivePortalApiUrl);
mResult = CaptivePortalProbeResult.failed(probeType);
}
@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();
}
}
sendProbe
final class HttpsProbe extends Probe {
HttpsProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
super(deps, proxy, url, captivePortalApiUrl);
}
@Override
protected CaptivePortalProbeResult sendProbe() {
return sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTPS);
}
}
通过dns解析,https同样也是走sendHttpProbe
private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
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);
}
这里主要看下Excepiton,当连接小米wifi的时候,因为访问的url并不在百度证书包含的内容之内所以直接会异常,httpResponseCode =CaptivePortalProbeResult.FAILED_CODE。
由此可知理想的位置应该在 catch之后二次作判断。
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(
TrafficStatsConstants.TAG_SYSTEM_PROBE);
try {
if (httpResponseCode == 200) {
} 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);
}
logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
final CaptivePortalProbeResult probeResult;
if (probeSpec == null) {
probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl,
url.toString(), 1 << probeType);
} else {
probeResult = probeSpec.getResult(httpResponseCode, redirectUrl);
}
return probeResult;
}