blob: 2d93e304dc2a4bd97d11c98a1ba5db75c43ec4a6 [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.util.ArrayMap;
import android.util.LocalLog;
import android.util.Log;
import com.android.wifi.resources.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This classes manages the addition and removal of BSSIDs to the BSSID blocklist, which is used
* for firmware roaming and network selection.
*/
public class BssidBlocklistMonitor {
// A special type association rejection
public static final int REASON_AP_UNABLE_TO_HANDLE_NEW_STA = 0;
// No internet
public static final int REASON_NETWORK_VALIDATION_FAILURE = 1;
// Wrong password error
public static final int REASON_WRONG_PASSWORD = 2;
// Incorrect EAP credentials
public static final int REASON_EAP_FAILURE = 3;
// Other association rejection failures
public static final int REASON_ASSOCIATION_REJECTION = 4;
// Associated timeout failures, when the RSSI is good
public static final int REASON_ASSOCIATION_TIMEOUT = 5;
// Other authentication failures
public static final int REASON_AUTHENTICATION_FAILURE = 6;
// DHCP failures
public static final int REASON_DHCP_FAILURE = 7;
// Abnormal disconnect error
public static final int REASON_ABNORMAL_DISCONNECT = 8;
// Constant being used to keep track of how many failure reasons there are.
public static final int NUMBER_REASON_CODES = 9;
@IntDef(prefix = { "REASON_" }, value = {
REASON_AP_UNABLE_TO_HANDLE_NEW_STA,
REASON_NETWORK_VALIDATION_FAILURE,
REASON_WRONG_PASSWORD,
REASON_EAP_FAILURE,
REASON_ASSOCIATION_REJECTION,
REASON_ASSOCIATION_TIMEOUT,
REASON_AUTHENTICATION_FAILURE,
REASON_DHCP_FAILURE
})
@Retention(RetentionPolicy.SOURCE)
public @interface FailureReason {}
// To be filled with values from the overlay.
private static final int[] FAILURE_COUNT_DISABLE_THRESHOLD = new int[NUMBER_REASON_CODES];
private boolean mFailureCountDisableThresholdArrayInitialized = false;
private static final String[] FAILURE_REASON_STRINGS = {
"REASON_AP_UNABLE_TO_HANDLE_NEW_STA",
"REASON_NETWORK_VALIDATION_FAILURE",
"REASON_WRONG_PASSWORD",
"REASON_EAP_FAILURE",
"REASON_ASSOCIATION_REJECTION",
"REASON_ASSOCIATION_TIMEOUT",
"REASON_AUTHENTICATION_FAILURE",
"REASON_DHCP_FAILURE",
"REASON_ABNORMAL_DISCONNECT"
};
private static final String FAILURE_BSSID_BLOCKED_BY_FRAMEWORK_REASON_STRING =
"BlockedByFramework";
private static final long ABNORMAL_DISCONNECT_RESET_TIME_MS = TimeUnit.HOURS.toMillis(3);
private static final String TAG = "BssidBlocklistMonitor";
private final Context mContext;
private final WifiLastResortWatchdog mWifiLastResortWatchdog;
private final WifiConnectivityHelper mConnectivityHelper;
private final Clock mClock;
private final LocalLog mLocalLog;
private final Calendar mCalendar;
private final WifiScoreCard mWifiScoreCard;
// Map of bssid to BssidStatus
private Map<String, BssidStatus> mBssidStatusMap = new ArrayMap<>();
/**
* Create a new instance of BssidBlocklistMonitor
*/
BssidBlocklistMonitor(Context context, WifiConnectivityHelper connectivityHelper,
WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog,
WifiScoreCard wifiScoreCard) {
mContext = context;
mConnectivityHelper = connectivityHelper;
mWifiLastResortWatchdog = wifiLastResortWatchdog;
mClock = clock;
mLocalLog = localLog;
mCalendar = Calendar.getInstance();
mWifiScoreCard = wifiScoreCard;
}
// A helper to log debugging information in the local log buffer, which can
// be retrieved in bugreport.
private void localLog(String log) {
mLocalLog.log(log);
}
/**
* calculates the blocklist duration based on the current failure streak with exponential
* backoff.
* @param failureStreak should be greater or equal to 0.
* @return duration to block the BSSID in milliseconds
*/
private long getBlocklistDurationWithExponentialBackoff(int failureStreak,
int baseBlocklistDurationMs) {
failureStreak = Math.min(failureStreak, mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorFailureStreakCap));
if (failureStreak < 1) {
return baseBlocklistDurationMs;
}
return (long) (Math.pow(2.0, (double) failureStreak) * baseBlocklistDurationMs);
}
/**
* Dump the local log buffer and other internal state of BssidBlocklistMonitor.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of BssidBlocklistMonitor");
pw.println("BssidBlocklistMonitor - Bssid blocklist Begin ----");
mBssidStatusMap.values().stream().forEach(entry -> pw.println(entry));
pw.println("BssidBlocklistMonitor - Bssid blocklist End ----");
}
private void addToBlocklist(@NonNull BssidStatus entry, long durationMs, String reasonString) {
entry.addToBlocklist(durationMs, reasonString);
localLog(TAG + " addToBlocklist: bssid=" + entry.bssid + ", ssid=" + entry.ssid
+ ", durationMs=" + durationMs + ", reason=" + reasonString);
}
/**
* increments the number of failures for the given bssid and returns the number of failures so
* far.
* @return the BssidStatus for the BSSID
*/
private @NonNull BssidStatus incrementFailureCountForBssid(
@NonNull String bssid, @NonNull String ssid, int reasonCode) {
BssidStatus status = getOrCreateBssidStatus(bssid, ssid);
status.incrementFailureCount(reasonCode);
return status;
}
/**
* Get the BssidStatus representing the BSSID or create a new one if it doesn't exist.
*/
private @NonNull BssidStatus getOrCreateBssidStatus(@NonNull String bssid,
@NonNull String ssid) {
BssidStatus status = mBssidStatusMap.get(bssid);
if (status == null || !ssid.equals(status.ssid)) {
if (status != null) {
localLog("getOrCreateBssidStatus: BSSID=" + bssid + ", SSID changed from "
+ status.ssid + " to " + ssid);
}
status = new BssidStatus(bssid, ssid);
mBssidStatusMap.put(bssid, status);
}
return status;
}
private boolean isValidNetworkAndFailureReason(String bssid, String ssid,
@FailureReason int reasonCode) {
if (bssid == null || ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)
|| bssid.equals(ClientModeImpl.SUPPLICANT_BSSID_ANY)
|| reasonCode < 0 || reasonCode >= NUMBER_REASON_CODES) {
Log.e(TAG, "Invalid input: BSSID=" + bssid + ", SSID=" + ssid
+ ", reasonCode=" + reasonCode);
return false;
}
return true;
}
private boolean shouldWaitForWatchdogToTriggerFirst(String bssid,
@FailureReason int reasonCode) {
boolean isWatchdogRelatedFailure = reasonCode == REASON_ASSOCIATION_REJECTION
|| reasonCode == REASON_AUTHENTICATION_FAILURE
|| reasonCode == REASON_DHCP_FAILURE;
return isWatchdogRelatedFailure && mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(bssid);
}
/**
* Block any attempts to auto-connect to the BSSID for the specified duration.
*/
public void blockBssidForDurationMs(@NonNull String bssid, @NonNull String ssid,
long durationMs) {
BssidStatus status = getOrCreateBssidStatus(bssid, ssid);
if (status.isInBlocklist
&& status.blocklistEndTimeMs - mClock.getWallClockMillis() > durationMs) {
// Return because this BSSID is already being blocked for a longer time.
return;
}
addToBlocklist(status, durationMs, FAILURE_BSSID_BLOCKED_BY_FRAMEWORK_REASON_STRING);
}
private String getFailureReasonString(@FailureReason int reasonCode) {
if (reasonCode >= FAILURE_REASON_STRINGS.length) {
return "REASON_UNKNOWN";
}
return FAILURE_REASON_STRINGS[reasonCode];
}
private int getFailureThresholdForReason(@FailureReason int reasonCode) {
if (mFailureCountDisableThresholdArrayInitialized) {
return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
}
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorApUnableToHandleNewStaThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_NETWORK_VALIDATION_FAILURE] =
mContext.getResources().getInteger(R.integer
.config_wifiBssidBlocklistMonitorNetworkValidationFailureThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_WRONG_PASSWORD] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorWrongPasswordThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_EAP_FAILURE] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorEapFailureThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_REJECTION] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorAssociationRejectionThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_TIMEOUT] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorAssociationTimeoutThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AUTHENTICATION_FAILURE] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorAuthenticationFailureThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_DHCP_FAILURE] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorDhcpFailureThreshold);
FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ABNORMAL_DISCONNECT] =
mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorAbnormalDisconnectThreshold);
mFailureCountDisableThresholdArrayInitialized = true;
return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
}
private boolean handleBssidConnectionFailureInternal(String bssid, String ssid,
@FailureReason int reasonCode, boolean isLowRssi) {
BssidStatus entry = incrementFailureCountForBssid(bssid, ssid, reasonCode);
int failureThreshold = getFailureThresholdForReason(reasonCode);
int currentStreak = mWifiScoreCard.getBssidBlocklistStreak(ssid, bssid, reasonCode);
if (currentStreak > 0 || entry.failureCount[reasonCode] >= failureThreshold) {
// To rule out potential device side issues, don't add to blocklist if
// WifiLastResortWatchdog is still not triggered
if (shouldWaitForWatchdogToTriggerFirst(bssid, reasonCode)) {
return false;
}
int baseBlockDurationMs = mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorBaseBlockDurationMs);
if ((reasonCode == REASON_ASSOCIATION_TIMEOUT
|| reasonCode == REASON_ABNORMAL_DISCONNECT) && isLowRssi) {
baseBlockDurationMs = mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistMonitorBaseLowRssiBlockDurationMs);
}
addToBlocklist(entry,
getBlocklistDurationWithExponentialBackoff(currentStreak, baseBlockDurationMs),
getFailureReasonString(reasonCode));
mWifiScoreCard.incrementBssidBlocklistStreak(ssid, bssid, reasonCode);
return true;
}
return false;
}
/**
* Note a failure event on a bssid and perform appropriate actions.
* @return True if the blocklist has been modified.
*/
public boolean handleBssidConnectionFailure(String bssid, String ssid,
@FailureReason int reasonCode, boolean isLowRssi) {
if (!isValidNetworkAndFailureReason(bssid, ssid, reasonCode)) {
return false;
}
if (reasonCode == REASON_ABNORMAL_DISCONNECT) {
long connectionTime = mWifiScoreCard.getBssidConnectionTimestampMs(ssid, bssid);
// only count disconnects that happen shortly after a connection.
if (mClock.getWallClockMillis() - connectionTime
> mContext.getResources().getInteger(
R.integer.config_wifiBssidBlocklistAbnormalDisconnectTimeWindowMs)) {
return false;
}
}
return handleBssidConnectionFailureInternal(bssid, ssid, reasonCode, isLowRssi);
}
/**
* Note a connection success event on a bssid and clear appropriate failure counters.
*/
public void handleBssidConnectionSuccess(@NonNull String bssid, @NonNull String ssid) {
/**
* First reset the blocklist streak.
* This needs to be done even if a BssidStatus is not found, since the BssidStatus may
* have been removed due to blocklist timeout.
*/
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_WRONG_PASSWORD);
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_EAP_FAILURE);
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_REJECTION);
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_TIMEOUT);
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AUTHENTICATION_FAILURE);
long connectionTime = mClock.getWallClockMillis();
long prevConnectionTime = mWifiScoreCard.setBssidConnectionTimestampMs(
ssid, bssid, connectionTime);
if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ABNORMAL_DISCONNECT);
}
BssidStatus status = mBssidStatusMap.get(bssid);
if (status == null) {
return;
}
// Clear the L2 failure counters
status.failureCount[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] = 0;
status.failureCount[REASON_WRONG_PASSWORD] = 0;
status.failureCount[REASON_EAP_FAILURE] = 0;
status.failureCount[REASON_ASSOCIATION_REJECTION] = 0;
status.failureCount[REASON_ASSOCIATION_TIMEOUT] = 0;
status.failureCount[REASON_AUTHENTICATION_FAILURE] = 0;
if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
status.failureCount[REASON_ABNORMAL_DISCONNECT] = 0;
}
}
/**
* Note a successful network validation on a BSSID and clear appropriate failure counters.
* And then remove the BSSID from blocklist.
*/
public void handleNetworkValidationSuccess(@NonNull String bssid, @NonNull String ssid) {
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_NETWORK_VALIDATION_FAILURE);
BssidStatus status = mBssidStatusMap.get(bssid);
if (status == null) {
return;
}
status.failureCount[REASON_NETWORK_VALIDATION_FAILURE] = 0;
/**
* Network validation may take more than 1 tries to succeed.
* remove the BSSID from blocklist to make sure we are not accidentally blocking good
* BSSIDs.
**/
status.removeFromBlocklist();
}
/**
* Note a successful DHCP provisioning and clear appropriate faliure counters.
*/
public void handleDhcpProvisioningSuccess(@NonNull String bssid, @NonNull String ssid) {
mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_DHCP_FAILURE);
BssidStatus status = mBssidStatusMap.get(bssid);
if (status == null) {
return;
}
status.failureCount[REASON_DHCP_FAILURE] = 0;
}
/**
* Note the removal of a network from the Wifi stack's internal database and reset
* appropriate failure counters.
* @param ssid
*/
public void handleNetworkRemoved(@NonNull String ssid) {
clearBssidBlocklistForSsid(ssid);
mWifiScoreCard.resetBssidBlocklistStreakForSsid(ssid);
}
/**
* Clears the blocklist for BSSIDs associated with the input SSID only.
* @param ssid
*/
public void clearBssidBlocklistForSsid(@NonNull String ssid) {
int prevSize = mBssidStatusMap.size();
mBssidStatusMap.entrySet().removeIf(e -> e.getValue().ssid.equals(ssid));
int diff = prevSize - mBssidStatusMap.size();
if (diff > 0) {
localLog(TAG + " clearBssidBlocklistForSsid: SSID=" + ssid
+ ", num BSSIDs cleared=" + diff);
}
}
/**
* Clears the BSSID blocklist and failure counters.
*/
public void clearBssidBlocklist() {
if (mBssidStatusMap.size() > 0) {
int prevSize = mBssidStatusMap.size();
mBssidStatusMap.clear();
localLog(TAG + " clearBssidBlocklist: num BSSIDs cleared="
+ (prevSize - mBssidStatusMap.size()));
}
}
/**
* @param ssid
* @return the number of BSSIDs currently in the blocklist for the |ssid|.
*/
public int getNumBlockedBssidsForSsid(@NonNull String ssid) {
return (int) updateAndGetBssidBlocklistInternal()
.filter(entry -> ssid.equals(entry.ssid)).count();
}
/**
* Gets the BSSIDs that are currently in the blocklist.
* @return Set of BSSIDs currently in the blocklist
*/
public Set<String> updateAndGetBssidBlocklist() {
return updateAndGetBssidBlocklistInternal()
.map(entry -> entry.bssid)
.collect(Collectors.toSet());
}
/**
* Removes expired BssidStatus entries and then return remaining entries in the blocklist.
* @return Stream of BssidStatus for BSSIDs that are in the blocklist.
*/
private Stream<BssidStatus> updateAndGetBssidBlocklistInternal() {
Stream.Builder<BssidStatus> builder = Stream.builder();
long curTime = mClock.getWallClockMillis();
mBssidStatusMap.entrySet().removeIf(e -> {
BssidStatus status = e.getValue();
if (status.isInBlocklist) {
if (status.blocklistEndTimeMs < curTime) {
return true;
}
builder.accept(status);
}
return false;
});
return builder.build();
}
/**
* Sends the BSSIDs belonging to the input SSID down to the firmware to prevent auto-roaming
* to those BSSIDs.
* @param ssid
*/
public void updateFirmwareRoamingConfiguration(@NonNull String ssid) {
if (!mConnectivityHelper.isFirmwareRoamingSupported()) {
return;
}
ArrayList<String> bssidBlocklist = updateAndGetBssidBlocklistInternal()
.filter(entry -> ssid.equals(entry.ssid))
.sorted((o1, o2) -> (int) (o2.blocklistEndTimeMs - o1.blocklistEndTimeMs))
.map(entry -> entry.bssid)
.collect(Collectors.toCollection(ArrayList::new));
int fwMaxBlocklistSize = mConnectivityHelper.getMaxNumBlacklistBssid();
if (fwMaxBlocklistSize <= 0) {
Log.e(TAG, "Invalid max BSSID blocklist size: " + fwMaxBlocklistSize);
return;
}
// Having the blocklist size exceeding firmware max limit is unlikely because we have
// already flitered based on SSID. But just in case this happens, we are prioritizing
// sending down BSSIDs blocked for the longest time.
if (bssidBlocklist.size() > fwMaxBlocklistSize) {
bssidBlocklist = new ArrayList<String>(bssidBlocklist.subList(0,
fwMaxBlocklistSize));
}
// plumb down to HAL
if (!mConnectivityHelper.setFirmwareRoamingConfiguration(bssidBlocklist,
new ArrayList<String>())) { // TODO(b/36488259): SSID whitelist management.
}
}
/**
* Helper class that counts the number of failures per BSSID.
*/
private class BssidStatus {
public final String bssid;
public final String ssid;
public final int[] failureCount = new int[NUMBER_REASON_CODES];
private String mBlockReason = ""; // reason of blocking for logging only
// The following are used to flag how long this BSSID stays in the blocklist.
public boolean isInBlocklist;
public long blocklistEndTimeMs;
BssidStatus(String bssid, String ssid) {
this.bssid = bssid;
this.ssid = ssid;
}
/**
* increments the failure count for the reasonCode by 1.
* @return the incremented failure count
*/
public int incrementFailureCount(int reasonCode) {
return ++failureCount[reasonCode];
}
/**
* Add this BSSID to blocklist for the specified duration.
* @param durationMs
*/
public void addToBlocklist(long durationMs, String blockReason) {
isInBlocklist = true;
blocklistEndTimeMs = mClock.getWallClockMillis() + durationMs;
mBlockReason = blockReason;
}
/**
* Remove this BSSID from the blocklist.
*/
public void removeFromBlocklist() {
isInBlocklist = false;
blocklistEndTimeMs = 0;
mBlockReason = "";
localLog(TAG + " removeFromBlocklist BSSID=" + bssid);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("BSSID=" + bssid);
sb.append(", SSID=" + ssid);
sb.append(", isInBlocklist=" + isInBlocklist);
if (isInBlocklist) {
sb.append(", blockReason=" + mBlockReason);
mCalendar.setTimeInMillis(blocklistEndTimeMs);
sb.append(", blocklistEndTimeMs="
+ String.format("%tm-%td %tH:%tM:%tS.%tL", mCalendar, mCalendar,
mCalendar, mCalendar, mCalendar, mCalendar));
}
return sb.toString();
}
}
}