网络有效性检测

20 阅读8分钟

🔍 Android 网络有效性检测(Network Validation)实现机制

核心架构

┌──────────────────────────────────────┐
│    ConnectivityService (CS)          │
│  • 网络连接后触发验证                  │
│  • 接收验证结果                        │
└──────────┬───────────────────────────┘
           │ 创建并通信
           ▼
┌──────────────────────────────────────┐
│    NetworkMonitor (NM)               │ ◄─ 独立进程(NetworkStack)
│  • 执行多种探测                       │
│  • 判断网络可用性                     │
│  • 检测Captive Portal                │
└──────────┬───────────────────────────┘
           │ 5种探测
    ┌──────┴──────┬─────────┬──────────┬───────┐
    ▼             ▼         ▼          ▼       ▼
┌──────┐  ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ DNS  │  │  HTTPS   │ │  HTTP  │ │Fallback│ │PrivDNS │
│ 探测  │  │  探测    │ │  探测  │ │ 探测   │ │ 探测   │
└──────┘  └──────────┘ └────────┘ └────────┘ └────────┘

1️⃣ 五种探测类型

        /**
         * The overall validation result for the Network being reported on.
         *
         * <p>The possible values for this key are:
         * {@link #NETWORK_VALIDATION_RESULT_INVALID},
         * {@link #NETWORK_VALIDATION_RESULT_VALID},
         * {@link #NETWORK_VALIDATION_RESULT_PARTIALLY_VALID},
         * {@link #NETWORK_VALIDATION_RESULT_SKIPPED}.
         *
         * @see android.net.NetworkCapabilities#NET_CAPABILITY_VALIDATED
         */
        @NetworkValidationResult
        public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";

        /** DNS probe. */
        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
        public static final int NETWORK_PROBE_DNS = 0x04;

        /** HTTP probe. */
        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
        public static final int NETWORK_PROBE_HTTP = 0x08;

        /** HTTPS probe. */
        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
        public static final int NETWORK_PROBE_HTTPS = 0x10;

        /** Captive portal fallback probe. */
        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_FALLBACK
        public static final int NETWORK_PROBE_FALLBACK = 0x20;

        /** Private DNS (DNS over TLS) probd. */
        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS
        public static final int NETWORK_PROBE_PRIVATE_DNS = 0x40;

        /** @hide */
        @IntDef(
                prefix = {"NETWORK_PROBE_"},
                value = {
                        NETWORK_PROBE_DNS,
                        NETWORK_PROBE_HTTP,
                        NETWORK_PROBE_HTTPS,
                        NETWORK_PROBE_FALLBACK,
                        NETWORK_PROBE_PRIVATE_DNS
                })
        @Retention(RetentionPolicy.SOURCE)
        public @interface NetworkProbe {}
探测类型目的标志位
DNS Probe验证 DNS 解析是否工作0x04
HTTP Probe检测 Captive Portal(强制门户)0x08
HTTPS Probe验证互联网连接性0x10
Fallback ProbeHTTP 备用探测0x20
Private DNS Probe验证 DoT (DNS over TLS)0x40

2️⃣ HTTP/HTTPS 探测 URL

默认探测 URL

    /**
     * The URL used for HTTP captive portal detection upon a new connection.
     * A 204 response code from the server is used for validation.
     *
     * @hide
     */
    public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url";
    private static final String DEFAULT_TEST_URL =
            "https://connectivitycheck.android.com/generate_204";
    public static final String TEST_HOST = "connectivitycheck.gstatic.com";
    public static final String HTTP_REQUEST =
            "GET /generate_204 HTTP/1.0\r\n" +
                    "Host: " + TEST_HOST + "\r\n" +
                    "Connection: keep-alive\r\n\r\n";

关键点:

  • HTTP URL: http://connectivitycheck.gstatic.com/generate_204
  • HTTPS URL: https://www.google.com/generate_204
  • 期望响应: HTTP 204 No Content (表示网络畅通,无内容返回)

204 响应的含义

    private void testForCaptivePortal() {
        mTestingThread = new Thread(new Runnable() {
            public void run() {
                // Give time for captive portal to open.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                if (isFinishing() || isDestroyed()) return;
                HttpURLConnection urlConnection = null;
                int httpResponseCode = 500;
                int oldTag = TrafficStats.getAndSetThreadStatsTag(
                        NetworkStackConstants.TAG_SYSTEM_PROBE);
                try {
                    urlConnection = (HttpURLConnection) mNetwork.openConnection(
                            new URL(mCm.getCaptivePortalServerUrl()));
                    urlConnection.setInstanceFollowRedirects(false);
                    urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
                    urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
                    urlConnection.setUseCaches(false);
                    urlConnection.getInputStream();
                    httpResponseCode = urlConnection.getResponseCode();
                } catch (IOException e) {
                    loge(e.getMessage());
                } finally {
                    if (urlConnection != null) urlConnection.disconnect();
                    TrafficStats.setThreadStatsTag(oldTag);
                }
                if (httpResponseCode == 204) {
                    done(true);
                }
            }
        });
        mTestingThread.start();
    }

3️⃣ 验证流程

标准网络(Valid Network)验证

        void setNetworkValid(boolean privateDnsProbeSent) {
            mNmValidationResult = NETWORK_VALIDATION_RESULT_VALID;
            mNmValidationRedirectUrl = null;
            int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS;
            if (privateDnsProbeSent) {
                probesSucceeded |= NETWORK_VALIDATION_PROBE_PRIVDNS;
            }
            // The probesCompleted equals to probesSucceeded for the case of valid network, so put
            // the same value into two different parameter of the method.
            setProbesStatus(probesSucceeded, probesSucceeded);
        }

成功条件: DNS + HTTPS 探测都成功 → 网络有效

Captive Portal 检测

        void setNetworkPortal(String redirectUrl, boolean privateDnsProbeSent) {
            setNetworkInvalid(privateDnsProbeSent);
            mNmValidationRedirectUrl = redirectUrl;
            // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
            // in the beginning, so the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet.
            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP;
            int probesSucceeded = VALIDATION_RESULT_INVALID;
            if (privateDnsProbeSent) {
                probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
            }
            setProbesStatus(probesCompleted, probesSucceeded);
        }

检测逻辑:

  1. HTTP 探测返回 302/307 重定向 → 检测到 Captive Portal
  2. redirectUrl 存储登录页面地址
  3. 系统弹出通知,引导用户登录

部分连接(Partial Connectivity)

        void setNetworkPartial() {
            mNmValidationResult = NETWORK_VALIDATION_RESULT_PARTIAL;
            mNmValidationRedirectUrl = null;
            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
                    | NETWORK_VALIDATION_PROBE_FALLBACK;
            int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK;
            setProbesStatus(probesCompleted, probesSucceeded);
        }

判断条件:

  • DNS 成功 + Fallback 成功
  • 但 HTTPS 失败
  • 部分可用(例如只能访问内网或特定站点)

4️⃣ 验证结果处理

        private void handleNetworkTested(
                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
            final boolean valid = (testResult & NETWORK_VALIDATION_RESULT_VALID) != 0;
            final boolean partial = (testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0;
            final boolean portal = !TextUtils.isEmpty(redirectUrl);

            // If there is any kind of working networking, then the NAI has been evaluated
            // once. {@see NetworkAgentInfo#setEvaluated}, which returns whether this is
            // the first time this ever happened.
            final boolean someConnectivity = (valid || partial || portal);
            final boolean becameEvaluated = someConnectivity && nai.setEvaluated();
            // Because of b/245893397, if the score is updated when updateCapabilities is called,
            // any callback that receives onAvailable for that rematch receives an extra caps
            // callback. To prevent that, update the score in the agent so the updates below won't
            // see an update to both caps and score at the same time.
            // TODO : fix b/245893397 and remove this.
            if (becameEvaluated) nai.updateScoreForNetworkAgentUpdate();

            if (!valid && shouldIgnoreValidationFailureAfterRoam(nai)) {
                // Assume the validation failure is due to a temporary failure after roaming
                // and ignore it. NetworkMonitor will continue to retry validation. If it
                // continues to fail after the block timeout expires, the network will be
                // marked unvalidated. If it succeeds, then validation state will not change.
                return;
            }

            final boolean wasValidated = nai.isValidated();
            final boolean wasPartial = nai.partialConnectivity();
            final boolean wasPortal = nai.captivePortalDetected();
            nai.setPartialConnectivity(partial);
            nai.setCaptivePortalDetected(portal);
            nai.updateScoreForNetworkAgentUpdate();
            final boolean partialConnectivityChanged = (wasPartial != partial);
            final boolean portalChanged = (wasPortal != portal);

            if (DBG) {
                final String logMsg = !TextUtils.isEmpty(redirectUrl)
                        ? " with redirect to " + redirectUrl
                        : "";
                final String statusMsg;
                if (valid) {
                    statusMsg = "passed";
                } else if (!TextUtils.isEmpty(redirectUrl)) {
                    statusMsg = "detected a portal";
                } else {
                    statusMsg = "failed";
                }
                log(nai.toShortString() + " validation " + statusMsg + logMsg);
            }
            if (valid != wasValidated) {
                final FullScore oldScore = nai.getScore();
                nai.setValidated(valid);
                updateCapabilities(oldScore, nai, nai.networkCapabilities);
                if (valid) {
                    handleFreshlyValidatedNetwork(nai);
                    // Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and
                    // LOST_INTERNET notifications if network becomes valid.
                    mNotifier.clearNotification(nai.network.getNetId(),
                            NotificationType.NO_INTERNET);
                    mNotifier.clearNotification(nai.network.getNetId(),
                            NotificationType.LOST_INTERNET);
                    mNotifier.clearNotification(nai.network.getNetId(),

处理流程:

  1. 解析结果: valid(有效) / partial(部分) / portal(强制门户)
  2. 更新状态: NetworkCapabilities 中的 NET_CAPABILITY_VALIDATED
  3. 触发回调: 通知应用网络状态变化
  4. 清除通知: 如果之前有"无网络"通知,则清除
  5. 网络切换: 如果当前网络失效,自动切换到其他可用网络

5️⃣ Private DNS 验证

    /**
     * Return whether validation is required for private DNS in strict mode.
     * @param nc Network capabilities of the network to test.
     */
    public static boolean isPrivateDnsValidationRequired(@NonNull final NetworkCapabilities nc) {
        final boolean isVcnManaged = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
                && !nc.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
        final boolean isOemPaid = nc.hasCapability(NET_CAPABILITY_OEM_PAID)
                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
        final boolean isDefaultCapable = nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
                && nc.hasCapability(NET_CAPABILITY_TRUSTED);

        // TODO: Consider requiring validation for DUN networks.
        if (nc.hasCapability(NET_CAPABILITY_INTERNET)
                && (isVcnManaged || isOemPaid || isDefaultCapable)) {
            return true;
        }

        // Test networks that also have one of the major transport types are attempting to replicate
        // that transport on a test interface (for example, test ethernet networks with
        // EthernetManager#setIncludeTestInterfaces). Run validation on them for realistic tests.
        // See also comments on EthernetManager#setIncludeTestInterfaces and on TestNetworkManager.
        if (nc.hasTransport(TRANSPORT_TEST) && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) && (
                nc.hasTransport(TRANSPORT_WIFI)
                || nc.hasTransport(TRANSPORT_CELLULAR)
                || nc.hasTransport(TRANSPORT_BLUETOOTH)
                || nc.hasTransport(TRANSPORT_ETHERNET))) {
            return true;
        }

        return false;
    }

验证条件:

  • 网络具有 INTERNET 能力
  • 是 VCN 管理 / OEM付费 / 默认可用网络
  • → 需要验证 Private DNS (DoT) 可用性

6️⃣ 配置参数

Captive Portal 模式

         * When detecting a captive portal, immediately disconnect from the
         * network and do not reconnect to that network in the future.
         *
         * @hide
         */
        public static final int CAPTIVE_PORTAL_MODE_AVOID = 2;

        /**
         * What to do when connecting a network that presents a captive portal.
         * Must be one of the CAPTIVE_PORTAL_MODE_* constants above.
         *
         * The default for this setting is CAPTIVE_PORTAL_MODE_PROMPT.
         * @hide
         */
        @Readable
        public static final String CAPTIVE_PORTAL_MODE = "captive_portal_mode";

        /**
         * Setting to turn off captive portal detection. Feature is enabled by
         * default and the setting needs to be set to 0 to disable it.
         *
模式行为
CAPTIVE_PORTAL_MODE_IGNORE0忽略检测
CAPTIVE_PORTAL_MODE_PROMPT1弹出登录提示(默认)
CAPTIVE_PORTAL_MODE_AVOID2立即断开并避免该网络

7️⃣ 超时机制

            "http://connectivitycheck.gstatic.com/generate_204";

    // TODO: create better separation between radio types and network types

    // how long to wait before switching back to a radio's default network
    private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
    // system property that can override the above value
    private static final String NETWORK_RESTORE_DELAY_PROP_NAME =
            "android.telephony.apn-restore";

    // How long to wait before putting up a "This network doesn't have an Internet connection,
    // connect anyway?" dialog after the user selects a network that doesn't validate.
    private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;

    // How long to wait before considering that a network is bad in the absence of any form
    // of connectivity (valid, partial, captive portal). If none has been detected after this
    // delay, the stack considers this network bad, which may affect how it's handled in ranking
    // according to config_networkAvoidBadWifi.
    // Timeout in case the "actively prefer bad wifi" feature is on
    private static final int ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS = 20 * 1000;
    // Timeout in case the "actively prefer bad wifi" feature is off
    private static final int DEFAULT_EVALUATION_TIMEOUT_MS = 8 * 1000;

    // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
    private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
    private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
    private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;

    // Delimiter used when creating the broadcast delivery group for sending
    // CONNECTIVITY_ACTION broadcast.
    private static final char DELIVERY_GROUP_KEY_DELIMITER = ';';

    // The maximum value for the blocking validation result, in milliseconds.
    public static final int MAX_VALIDATION_IGNORE_AFTER_ROAM_TIME_MS = 10000;
  • 提示延迟: 8秒无验证结果后提示用户
  • 评估超时: 8-20秒判定网络为"差"
  • Linger 延迟: 30秒保持连接以观察

🎯 总结

Android 网络有效性检测的完整流程:

1. 网络连接建立 (WiFi/移动数据)
          ↓
2. ConnectivityService 触发 NetworkMonitor
          ↓
3. NetworkMonitor 执行多种探测:
   ├─ DNS 解析测试
   ├─ HTTPS 探测 (验证互联网)
   ├─ HTTP 探测 (检测 Captive Portal)
   ├─ Fallback 探测 (备用)
   └─ Private DNS 探测 (DoT)
          ↓
4. 判断结果:
   ├─ 204 响应 → ✅ VALID (网络有效)
   ├─ 302/307 → 🌐 CAPTIVE PORTAL (需登录)
   ├─ DNS成功但HTTPS失败 → ⚠️ PARTIAL (部分可用)
   └─ 全部失败 → ❌ INVALID (无效)
          ↓
5. 更新 NetworkCapabilities
          ↓
6. 通知应用/触发网络切换

关键特性:

  • 多层验证: 不只依赖单一探测
  • 自动重试: 失败后定期重新验证
  • 用户友好: Captive Portal 自动弹出登录页面
  • 智能切换: 验证失败自动切换到其他网络
  • 隐私保护: 使用 DoT 验证 Private DNS

这就是 Android 确保用户始终能连接到"真正可用"的网络的核心机制! 🚀