Android版本:Android P(9.0)
STA端网络保持连接,状态变更为无Internet访问能力提示
STA端断开,并随后自动重连成功,状态变更为无Internet访问能力提示
打开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
消息;
结合代码来看,WifiStateMachine
在L2ConnectedState
中处理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.IpClientCallback
的onProvisioningFailure()
回调方法,导致STA端WLAN断开;
接下来跳转到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;
...
}
...
}
}
结合代码分析,可知这部分逻辑应该是这样的:
NetlinkTracker
从Netd
接收到这些改变的事件,并通过NetlinkTracker.Callback.update()
回调给到IpClient.mNetlinkTracker
;IpClient.mNetlinkTracker
收到update()
事件回调后,向IpClient
状态机发送EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息;IpClient
如果此时处于RunningState
,那么在处理EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息时,会触发onProvisioningFailure()
方法回调;onProvisioningFailure()
方法回调会通过构造时注册进来的IpClient.Callback
回调实例,通知到WifiStateMachine
,后者会执行断开逻辑;梳理完了整个流程,接下来就需要分析上面的第4步中最后一个疑点——onProvisioningFailure()
为何会执行;
前面已经分析到了: IpClient
在RunningState
下处理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()
这个方法主要完成了如下任务:
mNetlinkTracker.getLinkProperties()
获取当前探测到的最新的链路信息,并封装为LinkProperties
返回,赋值给netlinkLinkProperties
;netlinkLinkProperties
中的需要关注的信息(LinkAddresses
、RouteInfo
等)拷贝到newLp
这个局部变量中;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);
}
这里方法跳转比较多,概括一下:
setLinkProperties
由于需要一个ProvisioningChange
枚举的返回结果,因此不仅仅是将参数生拷贝到成员变量mLinkProperties
,在此之前需要调用compareProvisioning
方法对mLinkProperties
与传入参数newLp
进行对比,并将差异返回,赋值给局部常量delta
,后者也是整个setLinkProperties
方法的返回结果;compareProvisioning
方法主要通过对比两个传入参数的isProvisioned
方法返回结果,来判定这次变动是GAINED_PROVISIONING
,STILL_PROVISIONED
,STILL_NOT_PROVISIONED
,还是LOST_PROVISIONING
;compareProvisioning
方法内的局部变量ignoreIPv6ProvisioningLoss
不为true
,IPV6地址、网关、DNS的丢失也会导致返回结果为LOST_PROVISIONING
而此问题出现,就是满足了第3条条件所致;
由上可知,此行为是AOSP原生逻辑,旨在使STA自动规避无法上网的AP;
因此:
config_networkAvoidBadWifi
修改为0
即可;