分析连接某个小米路由器提示无网络/ConnectivityService保存wifi判断

1,801 阅读7分钟

小米wifi.jpg

前情提要

手上正在开发的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

代码流程

小米wifi.png

整个过程主要涉及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();
    }

接下来是两条支线

  1. CMD_PROBE_COMPLETE
  2. 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,这里涉及到两种思路

  1. 如果判断是wifi连接状态 重新评估一下valid状态
  2. 跟踪代码,分析是那里最终设置阻止重联

当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;
    }