[Android][WIFI]手机作AP,关闭移动网络后,STA端断开重连问题分析

[Android][WIFI]手机作AP,关闭移动网络后,STA端断开重连问题分析

背景描述

测试平台

Android版本:Android P(9.0)

复现步骤

  • 准备两台移动设备,一台作为AP,一台作为STA;
  • 作为AP的设备具备移动网络上网,热点网络分享能力;
  • 打开作为AP的设备的移动网络流量开关,再打开热点;
  • 作为STA的设备接入该AP,并等待其通路判断完成,确保Internet网络访问能力正常;
  • 关闭作为AP的设备的移动网络流量开关;
  • 观察

期望结果

STA端网络保持连接,状态变更为无Internet访问能力提示

实际结果

STA端断开,并随后自动重连成功,状态变更为无Internet访问能力提示

问题分析

WifiStateMachine

打开STA端WiFi Verbose log后,抓取日志,首先确认断开的原因:

01-04 10:01:38.920 24751 24837 D WifiStateMachine:  ConnectedState !CMD_IP_CONFIGURATION_LOST rt=2214117/2214117 0 0 failures: 0/9 7e:d2:c5:43:c8:03 bcn=0
01-04 10:01:38.920 24751 24837 D WifiStateMachine:  L2ConnectedState !CMD_IP_CONFIGURATION_LOST rt=2214117/2214117 0 0 failures: 0/9 7e:d2:c5:43:c8:03 bcn=0
...
01-04 10:01:38.921 25250 25250 D wpa_supplicant: wlan0: Request to deauthenticate - bssid=7e:d2:c5:43:c8:03 pending_bssid=00:00:00:00:00:00 reason=3 (DEAUTH_LEAVING) state=COMPLETED

发现为主动断开,而WifiStateMachine显示状态机在L2ConnectedState处理了CMD_IP_CONFIGURATION_LOST消息;
结合代码来看,WifiStateMachineL2ConnectedState中处理CMD_IP_CONFIGURATION_LOST消息的逻辑中包含handleIpConfigurationLost()方法,后者会调用WifiNative.disconnect()发起断开请求:

//frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
    private void handleIpConfigurationLost() {
        mWifiInfo.setInetAddress(null);
        mWifiInfo.setMeteredHint(false);

        mWifiConfigManager.updateNetworkSelectionStatus(mLastNetworkId,
                WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE);

        /* DHCP times out after about 30 seconds, we do a
         * disconnect thru supplicant, we will let autojoin retry connecting to the network
         */
        mWifiNative.disconnect(mInterfaceName);
    }

    ...
    
	class L2ConnectedState extends State {
		...
        @Override
        public boolean processMessage(Message message) {
			...
            switch (message.what) {
				...
                case CMD_IP_CONFIGURATION_LOST:
                    // Get Link layer stats so that we get fresh tx packet counters.
                    getWifiLinkLayerStats();
                    handleIpConfigurationLost();
                    reportConnectionAttemptEnd(
                            WifiMetrics.ConnectionEvent.FAILURE_DHCP,
                            WifiMetricsProto.ConnectionEvent.HLF_NONE);
                    transitionTo(mDisconnectingState);
                    break;
				...
            }

            return HANDLED;
        }
    }

而发送发送CMD_IP_CONFIGURATION_LOST消息是由构造IpClient时传递的IpClientCallback实例对象回调过来的:

//frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
    private IpClient mIpClient;
    ...
    private FrameworkFacade mFacade;
    ...
	class IpClientCallback extends IpClient.Callback {
		...
        @Override
        public void onProvisioningFailure(LinkProperties newLp) {
            mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_IP_CONFIGURATION_LOST);
            sendMessage(CMD_IP_CONFIGURATION_LOST);
        }
        ...
    }
    ...
	private void setupClientMode() {
		...
        mIpClient = mFacade.makeIpClient(mContext, mInterfaceName, new IpClientCallback());
        mIpClient.setMulticastFilter(true);
        ...
    }

到这里,基本可以确定,是IpClient侧的逻辑,触发了WifiStateMachine.IpClientCallbackonProvisioningFailure()回调方法,导致STA端WLAN断开;

接下来跳转到IpClient继续分析:

IpClient

IpClient侧通常日志较少,需要添加日志:

public class IpClient extends StateMachine {
    private static final boolean DBG = true;
    ...
    private void configureAndStartStateMachine() {
		...
        setInitialState(mStoppedState);
        setDbg("wlan0".equals(mInterfaceName));
		...
    }
    ...
}

复现后可知:

01-04 10:01:38.918 24751 25461 D IpClient.wlan0: handleMessage: E msg.what=6
...
01-04 10:01:38.918 24751 25461 D IpClient.wlan0: processMsg: RunningState
01-04 10:01:38.919 24751 25461 D IpClient.wlan0: Netlink-seen LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,]  Routes: [fe80::/64 -> :: wlan0,] DnsAddresses: [240e:476:bbc2:3fcf::e6,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0}, new LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,]  Routes: [fe80::/64 -> :: wlan0,192.168.98.0/24 -> 0.0.0.0 wlan0,0.0.0.0/0 -> 192.168.98.27 wlan0,] DnsAddresses: [192.168.98.27,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0 TcpBufferSizes: 524288,1048576,2097152,262144,524288,1048576}; old LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,]  Routes: [fe80::/64 -> :: wlan0,::/0 -> fe80::7cd2:c5ff:fe43:c803 wlan0,240e:476:bbc2:3fcf::/64 -> :: wlan0,192.168.98.0/24 -> 0.0.0.0 wlan0,0.0.0.0/0 -> 192.168.98.27 wlan0,] DnsAddresses: [240e:476:bbc2:3fcf::e6,192.168.98.27,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0 TcpBufferSizes: 524288,1048576,2097152,262144,524288,1048576}
01-04 10:01:38.920 24751 25461 D IpClient.wlan0: onProvisioningFailure()

第一句日志输出是在assembleLinkProperties()方法中:

//frameworks/base/services/net/java/android/net/ip/IpClient.java
	private LinkProperties assembleLinkProperties() {
        // [1] Create a new LinkProperties object to populate.
        LinkProperties newLp = new LinkProperties();
        newLp.setInterfaceName(mInterfaceName);

        // [2] Pull in data from netlink:
        //         - IPv4 addresses
        //         - IPv6 addresses
        //         - IPv6 routes
        //         - IPv6 DNS servers
        //
        // N.B.: this is fundamentally race-prone and should be fixed by
        // changing NetlinkTracker from a hybrid edge/level model to an
        // edge-only model, or by giving IpClient its own netlink socket(s)
        // so as to track all required information directly.
        LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
        newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
        for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
            newLp.addRoute(route);
        }
        addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());

        // [3] Add in data from DHCPv4, if available.
        //
        // mDhcpResults is never shared with any other owner so we don't have
        // to worry about concurrent modification.
        if (mDhcpResults != null) {
            for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
                newLp.addRoute(route);
            }
            addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
            newLp.setDomains(mDhcpResults.domains);

            if (mDhcpResults.mtu != 0) {
                newLp.setMtu(mDhcpResults.mtu);
            }
        }

        // [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
        if (!TextUtils.isEmpty(mTcpBufferSizes)) {
            newLp.setTcpBufferSizes(mTcpBufferSizes);
        }
        if (mHttpProxy != null) {
            newLp.setHttpProxy(mHttpProxy);
        }

        // [5] Add data from InitialConfiguration
        if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
            InitialConfiguration config = mConfiguration.mInitialConfig;
            // Add InitialConfiguration routes and dns server addresses once all addresses
            // specified in the InitialConfiguration have been observed with Netlink.
            if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
                for (IpPrefix prefix : config.directlyConnectedRoutes) {
                    newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
                }
            }
            addAllReachableDnsServers(newLp, config.dnsServers);
        }
        final LinkProperties oldLp = mLinkProperties;
        if (DBG) {
            Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
                    netlinkLinkProperties, newLp, oldLp));
        }

        // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
        // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
        return newLp;
    }

结合状态机的日志,可知是RunningState处理EVENT_NETLINK_LINKPROPERTIES_CHANGED消息时调用的assembleLinkProperties()方法:

//frameworks/base/services/net/java/android/net/ip/IpClient.java
	...
	class RunningState extends State {
        private ConnectivityPacketTracker mPacketTracker;
        private boolean mDhcpActionInFlight;
        ...
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
				...
                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
                        transitionTo(mStoppingState);
                    }
                    break;
				...
            }
			...
        }
    }

结合代码分析,可知这部分逻辑应该是这样的:

  1. AP端关闭移动网络流量开关后,STA端IPV6地址与路由表发生改变;
  2. NetlinkTrackerNetd接收到这些改变的事件,并通过NetlinkTracker.Callback.update()回调给到IpClient.mNetlinkTracker
  3. IpClient.mNetlinkTracker收到update()事件回调后,向IpClient状态机发送EVENT_NETLINK_LINKPROPERTIES_CHANGED消息;
  4. IpClient如果此时处于RunningState,那么在处理EVENT_NETLINK_LINKPROPERTIES_CHANGED消息时,会触发onProvisioningFailure()方法回调;
  5. onProvisioningFailure()方法回调会通过构造时注册进来的IpClient.Callback回调实例,通知到WifiStateMachine,后者会执行断开逻辑;

梳理完了整个流程,接下来就需要分析上面的第4步中最后一个疑点——onProvisioningFailure()为何会执行;

前面已经分析到了: IpClientRunningState下处理EVENT_NETLINK_LINKPROPERTIES_CHANGED消息会调用handleLinkPropertiesUpdate()方法,参数为SEND_CALLBACKS,恒为true

//frameworks/base/services/net/java/android/net/ip/IpClient.java
    private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) {
        switch (delta) {
			...
            case LOST_PROVISIONING:
                if (DBG) { Log.d(mTag, "onProvisioningFailure()"); }
                recordMetric(IpManagerEvent.PROVISIONING_FAIL);
                mCallback.onProvisioningFailure(newLp);
                break;
			...
        }
    }

    ...

    // Returns false if we have lost provisioning, true otherwise.
	private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
        final LinkProperties newLp = assembleLinkProperties();
        if (Objects.equals(newLp, mLinkProperties)) {
            return true;
        }
        final ProvisioningChange delta = setLinkProperties(newLp);
        if (sendCallbacks) {
            dispatchCallback(delta, newLp);
        }
        return (delta != ProvisioningChange.LOST_PROVISIONING);
    }

handleLinkPropertiesUpdate()方法实现内部,有一个名为delta的局部常量,类型为ProvisioningChange枚举,当delta这个局部常量赋值为setLinkProperties()方法的返回结果;

如果返回结果为LOST_PROVISIONING,则会通过dispatchCallback()方法触发onProvisioningFailure()回调,从而出现上面整个链路,导致STA断连;

handleLinkPropertiesUpdate()方法内部,最重要的两个方法是:assembleLinkProperties()setLinkProperties(newLp)

依次来看:

//frameworks/base/services/net/java/android/net/ip/IpClient.java

	private LinkProperties assembleLinkProperties() {
        // [1] Create a new LinkProperties object to populate.
        LinkProperties newLp = new LinkProperties();
        newLp.setInterfaceName(mInterfaceName);

        // [2] Pull in data from netlink:
        //         - IPv4 addresses
        //         - IPv6 addresses
        //         - IPv6 routes
        //         - IPv6 DNS servers
        //
        // N.B.: this is fundamentally race-prone and should be fixed by
        // changing NetlinkTracker from a hybrid edge/level model to an
        // edge-only model, or by giving IpClient its own netlink socket(s)
        // so as to track all required information directly.
        LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
        newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
        for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
            newLp.addRoute(route);
        }
        addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());

        // [3] Add in data from DHCPv4, if available.
        //
        // mDhcpResults is never shared with any other owner so we don't have
        // to worry about concurrent modification.
        if (mDhcpResults != null) {
            for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
                newLp.addRoute(route);
            }
            addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
            newLp.setDomains(mDhcpResults.domains);

            if (mDhcpResults.mtu != 0) {
                newLp.setMtu(mDhcpResults.mtu);
            }
        }

        // [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
        if (!TextUtils.isEmpty(mTcpBufferSizes)) {
            newLp.setTcpBufferSizes(mTcpBufferSizes);
        }
        if (mHttpProxy != null) {
            newLp.setHttpProxy(mHttpProxy);
        }

        // [5] Add data from InitialConfiguration
        if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
            InitialConfiguration config = mConfiguration.mInitialConfig;
            // Add InitialConfiguration routes and dns server addresses once all addresses
            // specified in the InitialConfiguration have been observed with Netlink.
            if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
                for (IpPrefix prefix : config.directlyConnectedRoutes) {
                    newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
                }
            }
            addAllReachableDnsServers(newLp, config.dnsServers);
        }
        final LinkProperties oldLp = mLinkProperties;
        if (DBG) {
            Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
                    netlinkLinkProperties, newLp, oldLp));
        }

        // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
        // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
        return newLp;
    }

assembleLinkProperties()这个方法主要完成了如下任务:

  1. 通过 mNetlinkTracker.getLinkProperties()获取当前探测到的最新的链路信息,并封装为LinkProperties返回,赋值给netlinkLinkProperties
  2. netlinkLinkProperties中的需要关注的信息(LinkAddressesRouteInfo等)拷贝到newLp这个局部变量中;
  3. 返回newLp

handleLinkPropertiesUpdate()方法内在收到assembleLinkProperties()方法的返回值后,会判断与当前的成员变量mLinkProperties是否相同;

LinkProperties重写了equals方法:

//frameworks/base/core/java/android/net/LinkProperties.java
	...
    @Override
    /**
     * Compares this {@code LinkProperties} instance against the target
     * LinkProperties in {@code obj}. Two LinkPropertieses are equal if
     * all their fields are equal in values.
     *
     * For collection fields, such as mDnses, containsAll() is used to check
     * if two collections contains the same elements, independent of order.
     * There are two thoughts regarding containsAll()
     * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal.
     * 2. Worst case performance is O(n^2).
     *
     * @param obj the object to be tested for equality.
     * @return {@code true} if both objects are equal, {@code false} otherwise.
     */
    public boolean equals(Object obj) {
        if (this == obj) return true;

        if (!(obj instanceof LinkProperties)) return false;

        LinkProperties target = (LinkProperties) obj;
        /**
         * This method does not check that stacked interfaces are equal, because
         * stacked interfaces are not so much a property of the link as a
         * description of connections between links.
         */
        return isIdenticalInterfaceName(target)
                && isIdenticalAddresses(target)
                && isIdenticalDnses(target)
                && isIdenticalPrivateDns(target)
                && isIdenticalValidatedPrivateDnses(target)
                && isIdenticalRoutes(target)
                && isIdenticalHttpProxy(target)
                && isIdenticalStackedLinks(target)
                && isIdenticalMtu(target)
                && isIdenticalTcpBufferSizes(target);
    }
    ...

由此可见,满足equals的要求非常苛刻,只要有任何变动,都会导致返回值为false

回到此问题,由于IPV6的相关地址变更,其关联的路由规则与地址信息均会发生改变,因此这里必然会返回false

由此,我们来看第二个重要的方法——setLinkProperties()

//frameworks/base/services/net/java/android/net/ip/IpClient.java
	...
    static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
        // For historical reasons, we should connect even if all we have is
        // an IPv4 address and nothing else.
        if (lp.hasIPv4Address() || lp.isProvisioned()) {
            return true;
        }
        if (config == null) {
            return false;
        }

        // When an InitialConfiguration is specified, ignore any difference with previous
        // properties and instead check if properties observed match the desired properties.
        return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
    }

	...

    private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
        ProvisioningChange delta;
        InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
        final boolean wasProvisioned = isProvisioned(oldLp, config);
        final boolean isProvisioned = isProvisioned(newLp, config);

        if (!wasProvisioned && isProvisioned) {
            delta = ProvisioningChange.GAINED_PROVISIONING;
        } else if (wasProvisioned && isProvisioned) {
            delta = ProvisioningChange.STILL_PROVISIONED;
        } else if (!wasProvisioned && !isProvisioned) {
            delta = ProvisioningChange.STILL_NOT_PROVISIONED;
        } else {
			...
            delta = ProvisioningChange.LOST_PROVISIONING;
        }

        final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned();
        final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address();
        final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute();
		...
        final boolean ignoreIPv6ProvisioningLoss = (mMultinetworkPolicyTracker != null)
                && !mMultinetworkPolicyTracker.getAvoidBadWifi();

		...
        if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
            delta = ProvisioningChange.LOST_PROVISIONING;
        }
		...
        if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
            delta = ProvisioningChange.LOST_PROVISIONING;
        }

        return delta;
    }

    // Updates all IpClient-related state concerned with LinkProperties.
    // Returns a ProvisioningChange for possibly notifying other interested
    // parties that are not fronted by IpClient.
    private ProvisioningChange setLinkProperties(LinkProperties newLp) {
		...
        ProvisioningChange delta = compareProvisioning(mLinkProperties, newLp);
        mLinkProperties = new LinkProperties(newLp);
		...
        return delta;
    }

	...

    // Returns false if we have lost provisioning, true otherwise.
    private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
		...
        final ProvisioningChange delta = setLinkProperties(newLp);
        if (sendCallbacks) {
            dispatchCallback(delta, newLp);
        }
        return (delta != ProvisioningChange.LOST_PROVISIONING);
    }

这里方法跳转比较多,概括一下:

  1. setLinkProperties由于需要一个ProvisioningChange枚举的返回结果,因此不仅仅是将参数生拷贝到成员变量mLinkProperties,在此之前需要调用compareProvisioning方法对mLinkProperties与传入参数newLp进行对比,并将差异返回,赋值给局部常量delta,后者也是整个setLinkProperties方法的返回结果;
  2. compareProvisioning方法主要通过对比两个传入参数的isProvisioned方法返回结果,来判定这次变动是GAINED_PROVISIONINGSTILL_PROVISIONEDSTILL_NOT_PROVISIONED,还是LOST_PROVISIONING
  3. 此外,如果compareProvisioning方法内的局部变量ignoreIPv6ProvisioningLoss不为true,IPV6地址、网关、DNS的丢失也会导致返回结果为LOST_PROVISIONING

而此问题出现,就是满足了第3条条件所致;

解决方案

由上可知,此行为是AOSP原生逻辑,旨在使STA自动规避无法上网的AP;

因此:

  1. 此问题若非强需求,可以不处理;
  2. 若需要屏蔽掉此行为,只需将config_networkAvoidBadWifi修改为0即可;

你可能感兴趣的:(Android,WIFI,Android,android,网络,java)