Add overlay support for hostapd

Adds support for reading an overlay file for LOHS to add additional
configurability.

Bug: 361651437
Test: included unit tests
Change-Id: Iff836d978c90c08144a4eaf746d0e0880f7c165b
diff --git a/hostapd/aidl/hostapd.cpp b/hostapd/aidl/hostapd.cpp
index b5118b3..58963cb 100644
--- a/hostapd/aidl/hostapd.cpp
+++ b/hostapd/aidl/hostapd.cpp
@@ -35,29 +35,9 @@
 #include "drivers/linux_ioctl.h"
 }
 
-// don't use hostapd's wpa_printf for unit testing. It won't compile otherwise
+
 #ifdef ANDROID_HOSTAPD_UNITTEST
-#include <android-base/logging.h>
-constexpr size_t logbuf_size = 8192;
-static ::android::base::LogSeverity wpa_to_android_level(int level)
-{
-	if (level == MSG_ERROR)
-		return ::android::base::ERROR;
-	if (level == MSG_WARNING)
-		return ::android::base::WARNING;
-	if (level == MSG_INFO)
-		return ::android::base::INFO;
-	return ::android::base::DEBUG;
-}
-void wpa_printf(int level, const char *fmt, ...) {
-	va_list ap;
-	va_start(ap, fmt);
-	char buffer[logbuf_size];
-	int res = snprintf(buffer, logbuf_size, fmt, ap);
-	if (res > 0 && res < logbuf_size) {
-		LOG(wpa_to_android_level(level)) << buffer;
-	}
-}
+#include "tests/unittest_overrides.h"
 #endif
 
 // The AIDL implementation for hostapd creates a hostapd.conf dynamically for
@@ -68,9 +48,78 @@
 namespace {
 constexpr char kConfFileNameFmt[] = "/data/vendor/wifi/hostapd/hostapd_%s.conf";
 
+/**
+ * To add an overlay file, add
+ *
+ * PRODUCT_COPY_FILES += \
+ *   <your/path/here>/hostapd_unmetered_overlay.conf:/vendor/etc/wifi/hostapd_unmetered_overlay.conf
+ *
+ * to the build file for your device, with the <your/path/here> being the path to your overlay in
+ * your repo. See the resolveVendorConfPath function in this file for more specifics on where this
+ * overlay file will wind up on your device.
+ *
+ * This overlay may configure any of the parameters listed in kOverlayableKeys. The kOverlayableKeys
+ * list is subject to change over time, as certain parameters may be added as APIs instead in the
+ * future.
+ *
+ * Example of what an overlay file might look like:
+ * $> cat hostapd_unmetered_overlay.conf
+ * dtim_period=2
+ * ap_max_inactivity=300
+ *
+ * Anything added to this overlay will be prepended to the hostapd.conf for unmetered (typically
+ * local only hotspots) interfaces.
+ */
+constexpr char kUnmeteredIfaceOverlayPath[] = "/etc/wifi/hostapd_unmetered_overlay.conf";
+
+/**
+ * Allow-list of hostapd.conf parameters (keys) that can be set via overlay.
+ *
+ * If introducing new APIs, be sure to remove keys from this list that would otherwise be
+ * controlled by the new API. This way we can avoid conflicting settings.
+ * Please file an FR to add new keys to this list.
+ */
+static const std::set<std::string> kOverlayableKeys = {
+	"ap_max_inactivity",
+	"assocresp_elements"
+	"beacon_int",
+	"disassoc_low_ack",
+	"dtim_period",
+	"fragm_threshold",
+	"max_listen_interval",
+	"max_num_sta",
+	"rts_threshold",
+	"skip_inactivity_poll",
+	"uapsd_advertisement_enabled",
+	"wmm_enabled",
+	"wmm_ac_vo_aifs",
+	"wmm_ac_vo_cwmin",
+	"wmm_ac_vo_cwmax",
+	"wmm_ac_vo_txop_limit",
+	"wmm_ac_vo_acm",
+	"wmm_ac_vi_aifs",
+	"wmm_ac_vi_cwmin",
+	"wmm_ac_vi_cwmax",
+	"wmm_ac_vi_txop_limit",
+	"wmm_ac_vi_acm",
+	"wmm_ac_bk_cwmin"
+	"wmm_ac_bk_cwmax"
+	"wmm_ac_bk_aifs",
+	"wmm_ac_bk_txop_limit",
+	"wmm_ac_bk_acm",
+	"wmm_ac_be_aifs",
+	"wmm_ac_be_cwmin",
+	"wmm_ac_be_cwmax",
+	"wmm_ac_be_txop_limit",
+	"wmm_ac_be_acm",
+};
+
 using android::base::RemoveFileIfExists;
 using android::base::StringPrintf;
+#ifndef ANDROID_HOSTAPD_UNITTEST
+using android::base::ReadFileToString;
 using android::base::WriteStringToFile;
+#endif
 using aidl::android::hardware::wifi::hostapd::BandMask;
 using aidl::android::hardware::wifi::hostapd::ChannelBandwidth;
 using aidl::android::hardware::wifi::hostapd::ChannelParams;
@@ -150,6 +199,31 @@
 	return true;
 }
 
+std::string resolveVendorConfPath(const std::string& conf_path)
+{
+#if defined(__ANDROID_APEX__)
+	// returns "/apex/<apexname>" + conf_path
+	std::string path = android::base::GetExecutablePath();
+	return path.substr(0, path.find_first_of('/', strlen("/apex/"))) + conf_path;
+#else
+	return std::string("/vendor") + conf_path;
+#endif
+}
+
+void logHostapdConfigError(int error, const std::string& file_path) {
+	wpa_printf(MSG_ERROR, "Cannot read/write hostapd config %s, error: %s", file_path.c_str(),
+			strerror(error));
+	struct stat st;
+	int result = stat(file_path.c_str(), &st);
+	if (result == 0) {
+		wpa_printf(MSG_ERROR, "hostapd config file uid: %d, gid: %d, mode: %d",st.st_uid,
+				st.st_gid, st.st_mode);
+	} else {
+		wpa_printf(MSG_ERROR, "Error calling stat() on hostapd config file: %s",
+				strerror(errno));
+	}
+}
+
 std::string WriteHostapdConfig(
     const std::string& instance_name, const std::string& config,
     const std::string br_name, const bool usesMlo)
@@ -168,21 +242,7 @@
 	}
 	// Diagnose failure
 	int error = errno;
-	wpa_printf(
-		MSG_ERROR, "Cannot write hostapd config to %s, error: %s",
-		file_path.c_str(), strerror(error));
-	struct stat st;
-	int result = stat(file_path.c_str(), &st);
-	if (result == 0) {
-		wpa_printf(
-			MSG_ERROR, "hostapd config file uid: %d, gid: %d, mode: %d",
-			st.st_uid, st.st_gid, st.st_mode);
-	} else {
-		wpa_printf(
-			MSG_ERROR,
-			"Error calling stat() on hostapd config file: %s",
-			strerror(errno));
-	}
+	logHostapdConfigError(errno, file_path);
 	return "";
 }
 
@@ -376,6 +436,14 @@
 	return mac_addr;
 }
 
+std::string trimWhitespace(const std::string& str) {
+	size_t pos = 0;
+	size_t len = str.size();
+	for (pos; pos < str.size() && std::isspace(str[pos]); ++pos){}
+	for (len; len - 1 > 0 && std::isspace(str[len-1]); --len){}
+	return str.substr(pos, len);
+}
+
 std::string CreateHostapdConfig(
 	const IfaceParams& iface_params,
 	const ChannelParams& channelParams,
@@ -785,7 +853,26 @@
 			isAidlServiceVersionAtLeast(3) && nw_params.isClientIsolationEnabled ?
 			"1" : "0");
 
+	// Overlay for LOHS (unmetered SoftAP)
+	std::string overlay_path = resolveVendorConfPath(kUnmeteredIfaceOverlayPath);
+	std::string overlay_string;
+	if (!nw_params.isMetered
+			&& 0 == access(overlay_path.c_str(), R_OK)
+			&& !ReadFileToString(overlay_path, &overlay_string)) {
+		logHostapdConfigError(errno, overlay_path);
+		return "";
+	}
+	std::string sanitized_overlay = "";
+	std::istringstream overlay_stream(overlay_string);
+	for (std::string line; std::getline(overlay_stream, line);) {
+		std::string overlay_key = trimWhitespace(line.substr(0, line.find("=")));
+		if (kOverlayableKeys.contains(overlay_key)) {
+			sanitized_overlay.append(line + "\n");
+		}
+	}
+
 	return StringPrintf(
+		"%s\n"
 		"interface=%s\n"
 		"driver=nl80211\n"
 		"ctrl_interface=/data/vendor/wifi/hostapd/ctrl_%s\n"
@@ -812,6 +899,7 @@
 		"%s\n"
 		"%s\n"
 		"%s\n",
+		sanitized_overlay.c_str(),
 		iface_params.usesMlo ? br_name.c_str() : iface_params.name.c_str(),
 		iface_params.name.c_str(),
 		ssid_as_string.c_str(),
diff --git a/hostapd/aidl/tests/unittest_overrides.h b/hostapd/aidl/tests/unittest_overrides.h
new file mode 100644
index 0000000..a5be178
--- /dev/null
+++ b/hostapd/aidl/tests/unittest_overrides.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#pragma once
+
+#include <android-base/logging.h>
+
+static ::android::base::LogSeverity wpa_to_android_level(int level)
+{
+	if (level == MSG_ERROR)
+		return ::android::base::ERROR;
+	if (level == MSG_WARNING)
+		return ::android::base::WARNING;
+	if (level == MSG_INFO)
+		return ::android::base::INFO;
+	return ::android::base::DEBUG;
+}
+
+// don't use hostapd's wpa_printf for unit testing. It won't compile otherwise
+void wpa_printf(int level, const char *fmt, ...) {
+	va_list ap;
+	va_start(ap, fmt);
+	LOG(wpa_to_android_level(level)) << ::android::base::StringPrintf(fmt, ap);
+	va_end(ap);
+}
+
+static int hostapd_unittest_stat_ret = 0;
+int stat(const char* pathname, struct stat* stabuf) {
+	if (hostapd_unittest_stat_ret != 0) {
+		errno = EINVAL;
+	}
+	return hostapd_unittest_stat_ret;
+}
+
+static int hostapd_unittest_accessRet = 0;
+int access(const char* pathname, int mode) {
+	if (hostapd_unittest_accessRet != 0) {
+		errno = EINVAL;
+	}
+	return hostapd_unittest_accessRet;
+}
+
+
+// You can inspect the string here to see what we tried to write to a file
+static std::string hostapd_unittest_config_output = "";
+static bool hostapd_unittest_WriteStringToFileRet = true;
+bool WriteStringToFile(const std::string& content, const std::string& path, mode_t mode,
+		uid_t owner, gid_t group) {
+	if (!hostapd_unittest_WriteStringToFileRet) {
+		errno = EINVAL;
+	} else {
+		hostapd_unittest_config_output = content;
+	}
+	return hostapd_unittest_WriteStringToFileRet;
+}
+
+// You can simulate a file having content with this string
+static std::string hostapd_unittest_overlay_content = "";
+static bool hostapd_unittest_ReadFileToStringRet = true;
+bool ReadFileToString(const std::string& path, std::string* content) {
+	*content = hostapd_unittest_overlay_content;
+	LOG(INFO) << "*content = " << *content;
+	return hostapd_unittest_ReadFileToStringRet;
+}
+
+/**
+ * We can simulate I/O operations failing by re-defining the calls.
+ *
+ * By default, all files are empty, and all calls succeed.
+ */
+void resetOverrides() {
+	hostapd_unittest_stat_ret = 0;
+	hostapd_unittest_WriteStringToFileRet = true;
+	hostapd_unittest_config_output = "";
+	hostapd_unittest_accessRet = 0;
+	hostapd_unittest_overlay_content = "";
+	hostapd_unittest_ReadFileToStringRet = true;
+}
diff --git a/hostapd/aidl/tests/unittests.cpp b/hostapd/aidl/tests/unittests.cpp
index 900f043..696e123 100644
--- a/hostapd/aidl/tests/unittests.cpp
+++ b/hostapd/aidl/tests/unittests.cpp
@@ -20,6 +20,111 @@
 #include "../hostapd.cpp"
 
 namespace aidl::android::hardware::wifi::hostapd {
+unsigned char kTestSsid[] = {0x31, 0x32, 0x33, 0x61, 0x62, 0x63, 0x64};
+
+class HostapdConfigTest : public testing::Test {
+	protected:
+	void SetUp() override {
+		resetOverrides();
+
+		mIface_params = {
+			.name = "wlan42",
+			.hwModeParams = {
+				.enable80211N = true,
+				.enable80211AC = false,
+				.enable80211AX = false,
+				.enable6GhzBand = false,
+				.enableHeSingleUserBeamformer = false,
+				.enableHeSingleUserBeamformee = false,
+				.enableHeMultiUserBeamformer = false,
+				.enableHeTargetWakeTime = false,
+				.enableEdmg = false,
+				.enable80211BE = false,
+				.maximumChannelBandwidth = ChannelBandwidth::BANDWIDTH_AUTO,
+			},
+			.channelParams = {},  // not used in config creation
+			.vendorData = {},  // not used in config creation
+			.instanceIdentities = {},  // not used in config creation
+			.usesMlo = false,
+		};
+		mChannel_params = {
+			.bandMask = BandMask::BAND_2_GHZ,
+			.acsChannelFreqRangesMhz = {},
+			.enableAcs = false,
+			.acsShouldExcludeDfs = false,
+			.channel = 6,
+		};
+		mNetwork_params = {
+			.ssid =  std::vector<uint8_t>(kTestSsid, kTestSsid + sizeof(kTestSsid)),
+			.isHidden = false,
+			.encryptionType = EncryptionType::WPA2,
+			.passphrase = "verysecurewowe",
+			.isMetered = true,  // default for tethered softap, change to false for lohs.
+			.vendorElements = {},
+		};
+	}
+
+	std::string mWlan42_tethered_config = "\ninterface=wlan42\n"
+		"driver=nl80211\n"
+		"ctrl_interface=/data/vendor/wifi/hostapd/ctrl_wlan42\n"
+		"ssid2=31323361626364\n"
+		"channel=6\n"
+		"op_class=83\n"
+		"ieee80211n=1\n"
+		"ieee80211ac=0\n\n\n"
+		"hw_mode=g\n\n"
+		"ignore_broadcast_ssid=0\n"
+		"wowlan_triggers=any\n"
+		"interworking=1\n"
+		"access_network_type=2\n\n"
+		"wpa=2\n"
+		"rsn_pairwise=CCMP\n"
+		"wpa_passphrase=verysecurewowe\n\n\n\n\n\n"
+		"ap_isolate=0\n";
+
+	std::string mWlan42_lohs_config = "dtim_period=2   \n"
+		"   ap_max_inactivity=300\n"
+		"skip_inactivity_poll = 1\n\n"
+		"interface=wlan42\n"
+		"driver=nl80211\n"
+		"ctrl_interface=/data/vendor/wifi/hostapd/ctrl_wlan42\n"
+		"ssid2=31323361626364\n"
+		"channel=6\n"
+		"op_class=83\n"
+		"ieee80211n=1\n"
+		"ieee80211ac=0\n\n\n"
+		"hw_mode=g\n\n"
+		"ignore_broadcast_ssid=0\n"
+		"wowlan_triggers=any\n"
+		"interworking=0\n\n"
+		"wpa=2\n"
+		"rsn_pairwise=CCMP\n"
+		"wpa_passphrase=verysecurewowe\n\n\n\n\n\n"
+		"ap_isolate=0\n";
+
+	std::string mWlan42_lohs_config_no_overlay = "\ninterface=wlan42\n"
+		"driver=nl80211\n"
+		"ctrl_interface=/data/vendor/wifi/hostapd/ctrl_wlan42\n"
+		"ssid2=31323361626364\n"
+		"channel=6\n"
+		"op_class=83\n"
+		"ieee80211n=1\n"
+		"ieee80211ac=0\n\n\n"
+		"hw_mode=g\n\n"
+		"ignore_broadcast_ssid=0\n"
+		"wowlan_triggers=any\n"
+		"interworking=0\n\n"
+		"wpa=2\n"
+		"rsn_pairwise=CCMP\n"
+		"wpa_passphrase=verysecurewowe\n\n\n\n\n\n"
+		"ap_isolate=0\n";
+
+	IfaceParams mIface_params;
+	ChannelParams mChannel_params;
+	NetworkParams mNetwork_params;
+	std::string mBr_name = "";
+	std::string mOwe_transition_ifname = "";
+};
 
 /**
  * Null hostapd_data* and null mac address (u8*)
@@ -59,7 +164,7 @@
 /**
  * There is a matching address and we return it.
  */
-TEST(getStaInfoByMacAddr, MatchingMac) {
+TEST(getStaInfoByMacAddrTest, MatchingMac) {
 	struct hostapd_data iface_hapd = {};
 	struct sta_info sta0 = {};
 	struct sta_info sta1 = {};
@@ -77,4 +182,65 @@
 	EXPECT_EQ(0, std::memcmp(sta_ptr_optional.value()->addr, sta1_addr, ETH_ALEN));
 }
 
+
+TEST_F(HostapdConfigTest, tetheredApConfig) {
+	// instance name, config string, br_name, usesMlo
+	std::string config_path = WriteHostapdConfig("wlan42", mWlan42_tethered_config, "", false);
+	std::string expected_path = "/data/vendor/wifi/hostapd/hostapd_wlan42.conf";
+	EXPECT_EQ(expected_path, config_path);
+	EXPECT_EQ(mWlan42_tethered_config, hostapd_unittest_config_output);
+}
+
+TEST_F(HostapdConfigTest, tetheredApConfigStatFails) {
+	hostapd_unittest_WriteStringToFileRet = false;
+	hostapd_unittest_stat_ret = -1;
+	// instance name, config string, br_name, usesMlo
+	std::string config_path = WriteHostapdConfig("wlan42", mWlan42_tethered_config, "", false);
+	std::string expected_path = "";
+	EXPECT_EQ(expected_path, config_path);
+}
+
+TEST_F(HostapdConfigTest, tetheredApConfigWriteFails) {
+	hostapd_unittest_WriteStringToFileRet = false;
+	// instance name, config string, br_name, usesMlo
+	std::string config_path = WriteHostapdConfig("wlan42", mWlan42_tethered_config, "", false);
+	std::string expected_path = "";
+	EXPECT_EQ(expected_path, config_path);
+}
+
+TEST_F(HostapdConfigTest, tetheredAp) {
+	std::string config_string = CreateHostapdConfig(mIface_params, mChannel_params, mNetwork_params,
+			mBr_name, mOwe_transition_ifname);
+	EXPECT_EQ(mWlan42_tethered_config, config_string);
+}
+
+TEST_F(HostapdConfigTest, lohsAp) {
+	mNetwork_params.isMetered = false;
+	hostapd_unittest_overlay_content =
+			"invalid_key=this_should_not_be_here\n"
+			"dtim_period=2   \n"
+			"   ap_max_inactivity=300\n"
+			"another_invalid_key_dtim_period=-10000\n"
+			"skip_inactivity_poll = 1";
+	std::string config_string = CreateHostapdConfig(mIface_params, mChannel_params, mNetwork_params,
+			mBr_name, mOwe_transition_ifname);
+	EXPECT_EQ(mWlan42_lohs_config, config_string);
+}
+
+TEST_F(HostapdConfigTest, lohsApAccessFails) {
+	mNetwork_params.isMetered = false;
+	hostapd_unittest_accessRet = -1;
+	std::string config_string = CreateHostapdConfig(mIface_params, mChannel_params, mNetwork_params,
+			mBr_name, mOwe_transition_ifname);
+	EXPECT_EQ(mWlan42_lohs_config_no_overlay, config_string);
+}
+
+TEST_F(HostapdConfigTest, lohsApReadFails) {
+	mNetwork_params.isMetered = false;
+	hostapd_unittest_ReadFileToStringRet = false;
+	std::string config_string = CreateHostapdConfig(mIface_params, mChannel_params, mNetwork_params,
+			mBr_name, mOwe_transition_ifname);
+	EXPECT_EQ("", config_string);
+}
+
 }  // namespace aidl::android::hardware::wifi::hostapd