/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_PRIMARY; import android.annotation.NonNull; import android.content.Context; import android.content.Intent; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.BatteryStatsManager; import android.os.Looper; import android.os.Message; import android.os.Parcelable; import android.os.UserHandle; import android.util.Log; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.io.FileDescriptor; import java.io.PrintWriter; /** * Tracks the state changes in supplicant and provides functionality * that is based on these state changes: * - detect a failed WPA handshake that loops indefinitely * - authentication failure handling * TODO(b/159944009): Need to rework this class to handle make before break transition on STA + * STA devices (Apps will not see SUPPLICANT_STATE_CHANGED_ACTION when switching wifi networks) */ public class SupplicantStateTracker extends StateMachine { private static final String TAG = "SupplicantStateTracker"; private final Context mContext; private final WifiConfigManager mWifiConfigManager; private final WifiMonitor mWifiMonitor; private final BatteryStatsManager mBatteryStatsManager; private final String mInterfaceName; private final ClientModeManager mClientModeManager; private final ClientModeManagerBroadcastQueue mBroadcastQueue; private boolean mVerboseLoggingEnabled = false; /* Indicates authentication failure in supplicant broadcast. * TODO: enhance auth failure reporting to include notification * for all type of failures: EAP, WPS & WPA networks */ private boolean mAuthFailureInSupplicantBroadcast = false; /* Authentication failure reason * see {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_NONE}, * {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_TIMEOUT}, * {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_WRONG_PSWD}, * {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_EAP_FAILURE} */ private int mAuthFailureReason; private final State mUninitializedState = new UninitializedState(); private final State mDefaultState = new DefaultState(); private final State mInactiveState = new InactiveState(); private final State mDisconnectState = new DisconnectedState(); private final State mScanState = new ScanState(); private final State mHandshakeState = new HandshakeState(); private final State mCompletedState = new CompletedState(); private final State mDormantState = new DormantState(); /** enable/disable verbose logging. */ public void enableVerboseLogging(boolean verbose) { mVerboseLoggingEnabled = verbose; } private String getTag() { return TAG + "[" + (mInterfaceName == null ? "unknown" : mInterfaceName) + "]"; } public SupplicantStateTracker( @NonNull Context context, @NonNull WifiConfigManager wifiConfigManager, @NonNull BatteryStatsManager batteryStatsManager, @NonNull Looper looper, @NonNull WifiMonitor wifiMonitor, @NonNull String interfaceName, @NonNull ClientModeManager clientModeManager, @NonNull ClientModeManagerBroadcastQueue broadcastQueue) { super(TAG, looper); mContext = context; mWifiConfigManager = wifiConfigManager; mBatteryStatsManager = batteryStatsManager; mWifiMonitor = wifiMonitor; mInterfaceName = interfaceName; mClientModeManager = clientModeManager; mBroadcastQueue = broadcastQueue; registerForWifiMonitorEvents(); addState(mDefaultState); { // disconnected states addState(mUninitializedState, mDefaultState); addState(mInactiveState, mDefaultState); addState(mDisconnectState, mDefaultState); // connected states addState(mScanState, mDefaultState); addState(mHandshakeState, mDefaultState); addState(mCompletedState, mDefaultState); addState(mDormantState, mDefaultState); } setInitialState(mUninitializedState); setLogRecSize(50); setLogOnlyTransitions(true); // start the state machine start(); } private static final int[] WIFI_MONITOR_EVENTS = { WifiMonitor.ASSOCIATION_REJECTION_EVENT, WifiMonitor.AUTHENTICATION_FAILURE_EVENT, WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT, }; private void registerForWifiMonitorEvents() { for (int event : WIFI_MONITOR_EVENTS) { mWifiMonitor.registerHandler(mInterfaceName, event, getHandler()); } } private void deregisterForWifiMonitorEvents() { for (int event : WIFI_MONITOR_EVENTS) { mWifiMonitor.deregisterHandler(mInterfaceName, event, getHandler()); } } /** * Called when the owner ClientModeImpl is stopped. No more actions shall be performed on this * instance after it is stopped. */ public void stop() { deregisterForWifiMonitorEvents(); quitNow(); } private void handleNetworkConnectionFailure(int netId, int disableReason) { if (mVerboseLoggingEnabled) { Log.d(getTag(), "handleNetworkConnectionFailure netId=" + netId + " reason " + disableReason); } /* update network status */ mWifiConfigManager.updateNetworkSelectionStatus(netId, disableReason); } private void transitionOnSupplicantStateChange(StateChangeResult stateChangeResult) { SupplicantState supState = stateChangeResult.state; if (mVerboseLoggingEnabled) { Log.d(getTag(), "Supplicant state: " + supState.toString() + "\n"); } switch (supState) { case DISCONNECTED: transitionTo(mDisconnectState); break; case INTERFACE_DISABLED: //we should have received a disconnection already, do nothing break; case SCANNING: transitionTo(mScanState); break; case AUTHENTICATING: case ASSOCIATING: case ASSOCIATED: case FOUR_WAY_HANDSHAKE: case GROUP_HANDSHAKE: transitionTo(mHandshakeState); break; case COMPLETED: transitionTo(mCompletedState); break; case DORMANT: transitionTo(mDormantState); break; case INACTIVE: transitionTo(mInactiveState); break; case UNINITIALIZED: case INVALID: transitionTo(mUninitializedState); break; default: Log.e(getTag(), "Unknown supplicant state " + supState); break; } } private static int supplicantStateToBatteryStatsSupplicantState(SupplicantState state) { switch (state) { case DISCONNECTED: return BatteryStatsManager.WIFI_SUPPL_STATE_DISCONNECTED; case INTERFACE_DISABLED: return BatteryStatsManager.WIFI_SUPPL_STATE_INTERFACE_DISABLED; case INACTIVE: return BatteryStatsManager.WIFI_SUPPL_STATE_INACTIVE; case SCANNING: return BatteryStatsManager.WIFI_SUPPL_STATE_SCANNING; case AUTHENTICATING: return BatteryStatsManager.WIFI_SUPPL_STATE_AUTHENTICATING; case ASSOCIATING: return BatteryStatsManager.WIFI_SUPPL_STATE_ASSOCIATING; case ASSOCIATED: return BatteryStatsManager.WIFI_SUPPL_STATE_ASSOCIATED; case FOUR_WAY_HANDSHAKE: return BatteryStatsManager.WIFI_SUPPL_STATE_FOUR_WAY_HANDSHAKE; case GROUP_HANDSHAKE: return BatteryStatsManager.WIFI_SUPPL_STATE_GROUP_HANDSHAKE; case COMPLETED: return BatteryStatsManager.WIFI_SUPPL_STATE_COMPLETED; case DORMANT: return BatteryStatsManager.WIFI_SUPPL_STATE_DORMANT; case UNINITIALIZED: return BatteryStatsManager.WIFI_SUPPL_STATE_UNINITIALIZED; case INVALID: return BatteryStatsManager.WIFI_SUPPL_STATE_INVALID; default: Log.w(TAG, "Unknown supplicant state " + state); return BatteryStatsManager.WIFI_SUPPL_STATE_INVALID; } } private void sendSupplicantStateChangedBroadcast( SupplicantState state, boolean failedAuth, int reasonCode) { int supplState = supplicantStateToBatteryStatsSupplicantState(state); if (mClientModeManager.getRole() == ROLE_CLIENT_PRIMARY) { mBatteryStatsManager.reportWifiSupplicantStateChanged(supplState, failedAuth); } String summary = "broadcast=SUPPLICANT_STATE_CHANGED_ACTION" + " state=" + state + " failedAuth=" + failedAuth + " reasonCode=" + reasonCode; if (mVerboseLoggingEnabled) Log.d(TAG, "Queuing " + summary); mBroadcastQueue.queueOrSendBroadcast( mClientModeManager, () -> { if (mVerboseLoggingEnabled) Log.d(TAG, "Sending " + summary); sendSupplicantStateChangedBroadcast(mContext, state, failedAuth, reasonCode); }); } private static void sendSupplicantStateChangedBroadcast( Context context, SupplicantState state, boolean failedAuth, int reasonCode) { Intent intent = new Intent(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_REPLACE_PENDING); intent.putExtra(WifiManager.EXTRA_NEW_STATE, (Parcelable) state); if (failedAuth) { intent.putExtra( WifiManager.EXTRA_SUPPLICANT_ERROR, WifiManager.ERROR_AUTHENTICATING); intent.putExtra( WifiManager.EXTRA_SUPPLICANT_ERROR_REASON, reasonCode); } context.sendStickyBroadcastAsUser(intent, UserHandle.ALL); } /******************************************************** * HSM states *******************************************************/ class DefaultState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } @Override public boolean processMessage(Message message) { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + message.toString() + "\n"); switch (message.what) { case WifiMonitor.AUTHENTICATION_FAILURE_EVENT: mAuthFailureInSupplicantBroadcast = true; mAuthFailureReason = message.arg1; break; case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT: StateChangeResult stateChangeResult = (StateChangeResult) message.obj; SupplicantState state = stateChangeResult.state; sendSupplicantStateChangedBroadcast(state, mAuthFailureInSupplicantBroadcast, mAuthFailureReason); mAuthFailureInSupplicantBroadcast = false; mAuthFailureReason = WifiManager.ERROR_AUTH_FAILURE_NONE; transitionOnSupplicantStateChange(stateChangeResult); break; case WifiMonitor.ASSOCIATION_REJECTION_EVENT: default: Log.e(getTag(), "Ignoring " + message); break; } return HANDLED; } } /* * This indicates that the supplicant state as seen * by the framework is not initialized yet. We are * in this state right after establishing a control * channel connection before any supplicant events * or after we have lost the control channel * connection to the supplicant */ class UninitializedState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } } class InactiveState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } } class DisconnectedState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } } class ScanState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } } class HandshakeState extends State { /** * The max number of the WPA supplicant loop iterations before we * decide that the loop should be terminated: */ private static final int MAX_SUPPLICANT_LOOP_ITERATIONS = 4; private int mLoopDetectIndex; private int mLoopDetectCount; @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); mLoopDetectIndex = 0; mLoopDetectCount = 0; } @Override public boolean processMessage(Message message) { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + message.toString() + "\n"); switch (message.what) { case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT: StateChangeResult stateChangeResult = (StateChangeResult) message.obj; SupplicantState state = stateChangeResult.state; if (SupplicantState.isHandshakeState(state)) { if (mLoopDetectIndex > state.ordinal()) { mLoopDetectCount++; } if (mLoopDetectCount > MAX_SUPPLICANT_LOOP_ITERATIONS) { Log.d(getTag(), "Supplicant loop detected, disabling network " + stateChangeResult.networkId); handleNetworkConnectionFailure(stateChangeResult.networkId, WifiConfiguration.NetworkSelectionStatus .DISABLED_AUTHENTICATION_FAILURE); } mLoopDetectIndex = state.ordinal(); sendSupplicantStateChangedBroadcast(state, mAuthFailureInSupplicantBroadcast, mAuthFailureReason); } else { //Have the DefaultState handle the transition return NOT_HANDLED; } break; default: return NOT_HANDLED; } return HANDLED; } } class CompletedState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } @Override public boolean processMessage(Message message) { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + message.toString() + "\n"); switch (message.what) { case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT: StateChangeResult stateChangeResult = (StateChangeResult) message.obj; SupplicantState state = stateChangeResult.state; sendSupplicantStateChangedBroadcast(state, mAuthFailureInSupplicantBroadcast, mAuthFailureReason); /* Ignore any connecting state in completed state. Group re-keying * events and other auth events that do not affect connectivity are * ignored */ if (SupplicantState.isConnecting(state)) { break; } transitionOnSupplicantStateChange(stateChangeResult); break; default: return NOT_HANDLED; } return HANDLED; } } //TODO: remove after getting rid of the state in supplicant class DormantState extends State { @Override public void enter() { if (mVerboseLoggingEnabled) Log.d(getTag(), getName() + "\n"); } } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { super.dump(fd, pw, args); pw.println("mAuthFailureInSupplicantBroadcast " + mAuthFailureInSupplicantBroadcast); pw.println("mAuthFailureReason " + mAuthFailureReason); pw.println(); } }