/* * Copyright (C) 2017 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.NonNull; import android.content.Context; import android.hardware.wifi.hostapd.V1_0.HostapdStatus; import android.hardware.wifi.hostapd.V1_0.HostapdStatusCode; import android.hardware.wifi.hostapd.V1_0.IHostapd; import android.hidl.manager.V1_0.IServiceManager; import android.hidl.manager.V1_0.IServiceNotification; import android.net.wifi.WifiConfiguration; import android.os.Handler; import android.os.HwRemoteBinder; import android.os.Looper; import android.os.RemoteException; import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wifi.WifiNative.HostapdDeathEventHandler; import com.android.server.wifi.util.ApConfigUtil; import com.android.server.wifi.util.NativeUtil; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.NoSuchElementException; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.ThreadSafe; /** * To maintain thread-safety, the locking protocol is that every non-static method (regardless of * access level) acquires mLock. */ @ThreadSafe public class HostapdHal { private static final String TAG = "HostapdHal"; @VisibleForTesting public static final String HAL_INSTANCE_NAME = "default"; @VisibleForTesting public static final long WAIT_FOR_DEATH_TIMEOUT_MS = 50L; private final Object mLock = new Object(); private boolean mVerboseLoggingEnabled = false; private final Handler mEventHandler; private final boolean mEnableAcs; private final boolean mEnableIeee80211AC; private final List mAcsChannelRanges; private boolean mForceApChannel = false; private int mForcedApChannel; // Hostapd HAL interface objects private IServiceManager mIServiceManager = null; private IHostapd mIHostapd; private HashMap mSoftApListeners = new HashMap<>(); private HostapdDeathEventHandler mDeathEventHandler; private ServiceManagerDeathRecipient mServiceManagerDeathRecipient; private HostapdDeathRecipient mHostapdDeathRecipient; // Death recipient cookie registered for current supplicant instance. private long mDeathRecipientCookie = 0; private final IServiceNotification mServiceNotificationCallback = new IServiceNotification.Stub() { public void onRegistration(String fqName, String name, boolean preexisting) { synchronized (mLock) { if (mVerboseLoggingEnabled) { Log.i(TAG, "IServiceNotification.onRegistration for: " + fqName + ", " + name + " preexisting=" + preexisting); } if (!initHostapdService()) { Log.e(TAG, "initalizing IHostapd failed."); hostapdServiceDiedHandler(mDeathRecipientCookie); } else { Log.i(TAG, "Completed initialization of IHostapd."); } } } }; private class ServiceManagerDeathRecipient implements HwRemoteBinder.DeathRecipient { @Override public void serviceDied(long cookie) { mEventHandler.post(() -> { synchronized (mLock) { Log.w(TAG, "IServiceManager died: cookie=" + cookie); hostapdServiceDiedHandler(mDeathRecipientCookie); mIServiceManager = null; // Will need to register a new ServiceNotification } }); } } private class HostapdDeathRecipient implements HwRemoteBinder.DeathRecipient { @Override public void serviceDied(long cookie) { mEventHandler.post(() -> { synchronized (mLock) { Log.w(TAG, "IHostapd/IHostapd died: cookie=" + cookie); hostapdServiceDiedHandler(cookie); } }); } } public HostapdHal(Context context, Looper looper) { mEventHandler = new Handler(looper); mEnableAcs = context.getResources().getBoolean(R.bool.config_wifi_softap_acs_supported); mEnableIeee80211AC = context.getResources().getBoolean(R.bool.config_wifi_softap_ieee80211ac_supported); mAcsChannelRanges = toAcsChannelRanges(context.getResources().getString( R.string.config_wifi_softap_acs_supported_channel_list)); mServiceManagerDeathRecipient = new ServiceManagerDeathRecipient(); mHostapdDeathRecipient = new HostapdDeathRecipient(); } /** * Enable/Disable verbose logging. * * @param enable true to enable, false to disable. */ void enableVerboseLogging(boolean enable) { synchronized (mLock) { mVerboseLoggingEnabled = enable; } } /** * Uses the IServiceManager to check if the device is running V1_1 of the HAL from the VINTF for * the device. * @return true if supported, false otherwise. */ private boolean isV1_1() { synchronized (mLock) { if (mIServiceManager == null) { Log.e(TAG, "isV1_1: called but mServiceManager is null!?"); return false; } try { return (mIServiceManager.getTransport( android.hardware.wifi.hostapd.V1_1.IHostapd.kInterfaceName, HAL_INSTANCE_NAME) != IServiceManager.Transport.EMPTY); } catch (RemoteException e) { Log.e(TAG, "Exception while operating on IServiceManager: " + e); handleRemoteException(e, "getTransport"); return false; } } } /** * Link to death for IServiceManager object. * @return true on success, false otherwise. */ private boolean linkToServiceManagerDeath() { synchronized (mLock) { if (mIServiceManager == null) return false; try { if (!mIServiceManager.linkToDeath(mServiceManagerDeathRecipient, 0)) { Log.wtf(TAG, "Error on linkToDeath on IServiceManager"); hostapdServiceDiedHandler(mDeathRecipientCookie); mIServiceManager = null; // Will need to register a new ServiceNotification return false; } } catch (RemoteException e) { Log.e(TAG, "IServiceManager.linkToDeath exception", e); mIServiceManager = null; // Will need to register a new ServiceNotification return false; } return true; } } /** * Registers a service notification for the IHostapd service, which triggers intialization of * the IHostapd * @return true if the service notification was successfully registered */ public boolean initialize() { synchronized (mLock) { if (mVerboseLoggingEnabled) { Log.i(TAG, "Registering IHostapd service ready callback."); } mIHostapd = null; if (mIServiceManager != null) { // Already have an IServiceManager and serviceNotification registered, don't // don't register another. return true; } try { mIServiceManager = getServiceManagerMockable(); if (mIServiceManager == null) { Log.e(TAG, "Failed to get HIDL Service Manager"); return false; } if (!linkToServiceManagerDeath()) { return false; } /* TODO(b/33639391) : Use the new IHostapd.registerForNotifications() once it exists */ if (!mIServiceManager.registerForNotifications( IHostapd.kInterfaceName, "", mServiceNotificationCallback)) { Log.e(TAG, "Failed to register for notifications to " + IHostapd.kInterfaceName); mIServiceManager = null; // Will need to register a new ServiceNotification return false; } } catch (RemoteException e) { Log.e(TAG, "Exception while trying to register a listener for IHostapd service: " + e); hostapdServiceDiedHandler(mDeathRecipientCookie); mIServiceManager = null; // Will need to register a new ServiceNotification return false; } return true; } } /** * Link to death for IHostapd object. * @return true on success, false otherwise. */ private boolean linkToHostapdDeath(HwRemoteBinder.DeathRecipient deathRecipient, long cookie) { synchronized (mLock) { if (mIHostapd == null) return false; try { if (!mIHostapd.linkToDeath(deathRecipient, cookie)) { Log.wtf(TAG, "Error on linkToDeath on IHostapd"); hostapdServiceDiedHandler(mDeathRecipientCookie); return false; } } catch (RemoteException e) { Log.e(TAG, "IHostapd.linkToDeath exception", e); return false; } return true; } } private boolean registerCallback( android.hardware.wifi.hostapd.V1_1.IHostapdCallback callback) { synchronized (mLock) { String methodStr = "registerCallback_1_1"; try { android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 = getHostapdMockableV1_1(); if (iHostapdV1_1 == null) return false; HostapdStatus status = iHostapdV1_1.registerCallback(callback); return checkStatusAndLogFailure(status, methodStr); } catch (RemoteException e) { handleRemoteException(e, methodStr); return false; } } } /** * Initialize the IHostapd object. * @return true on success, false otherwise. */ private boolean initHostapdService() { synchronized (mLock) { try { mIHostapd = getHostapdMockable(); } catch (RemoteException e) { Log.e(TAG, "IHostapd.getService exception: " + e); return false; } catch (NoSuchElementException e) { Log.e(TAG, "IHostapd.getService exception: " + e); return false; } if (mIHostapd == null) { Log.e(TAG, "Got null IHostapd service. Stopping hostapd HIDL startup"); return false; } if (!linkToHostapdDeath(mHostapdDeathRecipient, ++mDeathRecipientCookie)) { mIHostapd = null; return false; } // Register for callbacks for 1.1 hostapd. if (isV1_1() && !registerCallback(new HostapdCallback())) { mIHostapd = null; return false; } } return true; } /** * Enable force-soft-AP-channel mode which takes effect when soft AP starts next time * @param forcedApChannel The forced IEEE channel number */ void enableForceSoftApChannel(int forcedApChannel) { mForceApChannel = true; mForcedApChannel = forcedApChannel; } /** * Disable force-soft-AP-channel mode which take effect when soft AP starts next time */ void disableForceSoftApChannel() { mForceApChannel = false; } /** * Add and start a new access point. * * @param ifaceName Name of the interface. * @param config Configuration to use for the AP. * @param listener Callback for AP events. * @return true on success, false otherwise. */ public boolean addAccessPoint(@NonNull String ifaceName, @NonNull WifiConfiguration config, @NonNull WifiNative.SoftApListener listener) { synchronized (mLock) { final String methodStr = "addAccessPoint"; IHostapd.IfaceParams ifaceParams = new IHostapd.IfaceParams(); ifaceParams.ifaceName = ifaceName; ifaceParams.hwModeParams.enable80211N = true; ifaceParams.hwModeParams.enable80211AC = mEnableIeee80211AC; try { ifaceParams.channelParams.band = getBand(config); } catch (IllegalArgumentException e) { Log.e(TAG, "Unrecognized apBand " + config.apBand); return false; } if (mForceApChannel) { ifaceParams.channelParams.enableAcs = false; ifaceParams.channelParams.channel = mForcedApChannel; if (mForcedApChannel <= ApConfigUtil.HIGHEST_2G_AP_CHANNEL) { ifaceParams.channelParams.band = IHostapd.Band.BAND_2_4_GHZ; } else { ifaceParams.channelParams.band = IHostapd.Band.BAND_5_GHZ; } } else if (mEnableAcs) { ifaceParams.channelParams.enableAcs = true; ifaceParams.channelParams.acsShouldExcludeDfs = true; } else { // Downgrade IHostapd.Band.BAND_ANY to IHostapd.Band.BAND_2_4_GHZ if ACS // is not supported. // We should remove this workaround once channel selection is moved from // ApConfigUtil to here. if (ifaceParams.channelParams.band == IHostapd.Band.BAND_ANY) { Log.d(TAG, "ACS is not supported on this device, using 2.4 GHz band."); ifaceParams.channelParams.band = IHostapd.Band.BAND_2_4_GHZ; } ifaceParams.channelParams.enableAcs = false; ifaceParams.channelParams.channel = config.apChannel; } IHostapd.NetworkParams nwParams = new IHostapd.NetworkParams(); // TODO(b/67745880) Note that config.SSID is intended to be either a // hex string or "double quoted". // However, it seems that whatever is handing us these configurations does not obey // this convention. nwParams.ssid.addAll(NativeUtil.stringToByteArrayList(config.SSID)); nwParams.isHidden = config.hiddenSSID; nwParams.encryptionType = getEncryptionType(config); nwParams.pskPassphrase = (config.preSharedKey != null) ? config.preSharedKey : ""; if (!checkHostapdAndLogFailure(methodStr)) return false; try { HostapdStatus status; if (isV1_1()) { android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams ifaceParams1_1 = new android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams(); ifaceParams1_1.V1_0 = ifaceParams; if (mEnableAcs) { ifaceParams1_1.channelParams.acsChannelRanges.addAll(mAcsChannelRanges); } android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 = getHostapdMockableV1_1(); if (iHostapdV1_1 == null) return false; status = iHostapdV1_1.addAccessPoint_1_1(ifaceParams1_1, nwParams); } else { status = mIHostapd.addAccessPoint(ifaceParams, nwParams); } if (!checkStatusAndLogFailure(status, methodStr)) { return false; } mSoftApListeners.put(ifaceName, listener); return true; } catch (RemoteException e) { handleRemoteException(e, methodStr); return false; } } } /** * Remove a previously started access point. * * @param ifaceName Name of the interface. * @return true on success, false otherwise. */ public boolean removeAccessPoint(@NonNull String ifaceName) { synchronized (mLock) { final String methodStr = "removeAccessPoint"; if (!checkHostapdAndLogFailure(methodStr)) return false; try { HostapdStatus status = mIHostapd.removeAccessPoint(ifaceName); if (!checkStatusAndLogFailure(status, methodStr)) { return false; } mSoftApListeners.remove(ifaceName); return true; } catch (RemoteException e) { handleRemoteException(e, methodStr); return false; } } } /** * Registers a death notification for hostapd. * @return Returns true on success. */ public boolean registerDeathHandler(@NonNull HostapdDeathEventHandler handler) { if (mDeathEventHandler != null) { Log.e(TAG, "Death handler already present"); } mDeathEventHandler = handler; return true; } /** * Deregisters a death notification for hostapd. * @return Returns true on success. */ public boolean deregisterDeathHandler() { if (mDeathEventHandler == null) { Log.e(TAG, "No Death handler present"); } mDeathEventHandler = null; return true; } /** * Clear internal state. */ private void clearState() { synchronized (mLock) { mIHostapd = null; } } /** * Handle hostapd death. */ private void hostapdServiceDiedHandler(long cookie) { synchronized (mLock) { if (mDeathRecipientCookie != cookie) { Log.i(TAG, "Ignoring stale death recipient notification"); return; } clearState(); if (mDeathEventHandler != null) { mDeathEventHandler.onDeath(); } } } /** * Signals whether Initialization completed successfully. */ public boolean isInitializationStarted() { synchronized (mLock) { return mIServiceManager != null; } } /** * Signals whether Initialization completed successfully. */ public boolean isInitializationComplete() { synchronized (mLock) { return mIHostapd != null; } } /** * Start the hostapd daemon. * * @return true on success, false otherwise. */ public boolean startDaemon() { synchronized (mLock) { try { // This should startup hostapd daemon using the lazy start HAL mechanism. getHostapdMockable(); } catch (RemoteException e) { Log.e(TAG, "Exception while trying to start hostapd: " + e); hostapdServiceDiedHandler(mDeathRecipientCookie); return false; } catch (NoSuchElementException e) { // We're starting the daemon, so expect |NoSuchElementException|. Log.d(TAG, "Successfully triggered start of hostapd using HIDL"); } return true; } } /** * Terminate the hostapd daemon & wait for it's death. */ public void terminate() { synchronized (mLock) { // Register for a new death listener to block until hostapd is dead. final long waitForDeathCookie = new Random().nextLong(); final CountDownLatch waitForDeathLatch = new CountDownLatch(1); linkToHostapdDeath((cookie) -> { Log.d(TAG, "IHostapd died: cookie=" + cookie); if (cookie != waitForDeathCookie) return; waitForDeathLatch.countDown(); }, waitForDeathCookie); final String methodStr = "terminate"; if (!checkHostapdAndLogFailure(methodStr)) return; try { mIHostapd.terminate(); } catch (RemoteException e) { handleRemoteException(e, methodStr); } // Now wait for death listener callback to confirm that it's dead. try { if (!waitForDeathLatch.await(WAIT_FOR_DEATH_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { Log.w(TAG, "Timed out waiting for confirmation of hostapd death"); } } catch (InterruptedException e) { Log.w(TAG, "Failed to wait for hostapd death"); } } } /** * Wrapper functions to access static HAL methods, created to be mockable in unit tests */ @VisibleForTesting protected IServiceManager getServiceManagerMockable() throws RemoteException { synchronized (mLock) { return IServiceManager.getService(); } } @VisibleForTesting protected IHostapd getHostapdMockable() throws RemoteException { synchronized (mLock) { return IHostapd.getService(); } } @VisibleForTesting protected android.hardware.wifi.hostapd.V1_1.IHostapd getHostapdMockableV1_1() throws RemoteException { synchronized (mLock) { try { return android.hardware.wifi.hostapd.V1_1.IHostapd.castFrom(mIHostapd); } catch (NoSuchElementException e) { Log.e(TAG, "Failed to get IHostapd", e); return null; } } } private static int getEncryptionType(WifiConfiguration localConfig) { int encryptionType; switch (localConfig.getAuthType()) { case WifiConfiguration.KeyMgmt.NONE: encryptionType = IHostapd.EncryptionType.NONE; break; case WifiConfiguration.KeyMgmt.WPA_PSK: encryptionType = IHostapd.EncryptionType.WPA; break; case WifiConfiguration.KeyMgmt.WPA2_PSK: encryptionType = IHostapd.EncryptionType.WPA2; break; default: // We really shouldn't default to None, but this was how NetworkManagementService // used to do this. encryptionType = IHostapd.EncryptionType.NONE; break; } return encryptionType; } private static int getBand(WifiConfiguration localConfig) { int bandType; switch (localConfig.apBand) { case WifiConfiguration.AP_BAND_2GHZ: bandType = IHostapd.Band.BAND_2_4_GHZ; break; case WifiConfiguration.AP_BAND_5GHZ: bandType = IHostapd.Band.BAND_5_GHZ; break; case WifiConfiguration.AP_BAND_ANY: bandType = IHostapd.Band.BAND_ANY; break; default: throw new IllegalArgumentException(); } return bandType; } /** * Convert channel list string like '1-6,11' to list of AcsChannelRanges */ private List toAcsChannelRanges(String channelListStr) { ArrayList acsChannelRanges = new ArrayList<>(); String[] channelRanges = channelListStr.split(","); for (String channelRange : channelRanges) { android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange acsChannelRange = new android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange(); try { if (channelRange.contains("-")) { String[] channels = channelRange.split("-"); if (channels.length != 2) { Log.e(TAG, "Unrecognized channel range, length is " + channels.length); continue; } int start = Integer.parseInt(channels[0]); int end = Integer.parseInt(channels[1]); if (start > end) { Log.e(TAG, "Invalid channel range, from " + start + " to " + end); continue; } acsChannelRange.start = start; acsChannelRange.end = end; } else { acsChannelRange.start = Integer.parseInt(channelRange); acsChannelRange.end = acsChannelRange.start; } } catch (NumberFormatException e) { // Ignore malformed value Log.e(TAG, "Malformed channel value detected: " + e); continue; } acsChannelRanges.add(acsChannelRange); } return acsChannelRanges; } /** * Returns false if Hostapd is null, and logs failure to call methodStr */ private boolean checkHostapdAndLogFailure(String methodStr) { synchronized (mLock) { if (mIHostapd == null) { Log.e(TAG, "Can't call " + methodStr + ", IHostapd is null"); return false; } return true; } } /** * Returns true if provided status code is SUCCESS, logs debug message and returns false * otherwise */ private boolean checkStatusAndLogFailure(HostapdStatus status, String methodStr) { synchronized (mLock) { if (status.code != HostapdStatusCode.SUCCESS) { Log.e(TAG, "IHostapd." + methodStr + " failed: " + status.code + ", " + status.debugMessage); return false; } else { if (mVerboseLoggingEnabled) { Log.d(TAG, "IHostapd." + methodStr + " succeeded"); } return true; } } } private void handleRemoteException(RemoteException e, String methodStr) { synchronized (mLock) { hostapdServiceDiedHandler(mDeathRecipientCookie); Log.e(TAG, "IHostapd." + methodStr + " failed with exception", e); } } private class HostapdCallback extends android.hardware.wifi.hostapd.V1_1.IHostapdCallback.Stub { @Override public void onFailure(String ifaceName) { Log.w(TAG, "Failure on iface " + ifaceName); WifiNative.SoftApListener listener = mSoftApListeners.get(ifaceName); if (listener != null) { listener.onFailure(); } } } }