From f5ba6681fa3f980eeb5109726bf00d34c50c560c Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Tue, 21 Jul 2026 22:33:15 +0300 Subject: [PATCH] platform: harden stock OpenWrt Wi-Fi profiles Let the stock UCI backend use explicit multi-band BSS profiles, merge identical profiles, and reject incomplete or invalid input. Keep the legacy wireless-section path available when explicit profiles are not requested. Secured profiles must carry a plausible key: an 8..63 character passphrase or a 64 hex digit PSK, anything else is rejected. An explicit profile must state its encryption; an omitted option is an error instead of silently configuring an open network. Also avoid building platform tests in non-test builds and make the association-frame bitfields portable across the OpenWrt toolchain. The whole series is publicly reviewable at https://gitlab.com/kreout/prpl-mesh-mercusys/-/merge_requests/1; the upstream GitLab only accepts merge requests from project members, so the upstream submission itself goes through the prpl Foundation Jira. Signed-off-by: Andrei Ovcharenko --- .../platform/bpl/cfg/uci/bpl_cfg_wifi.cpp | 215 +++++++++++------- .../platform/bpl/cfg/uci/bpl_cfg_wifi_utils.h | 139 +++++++++++ framework/platform/bpl/test/CMakeLists.txt | 8 + .../bpl/test/bpl_cfg_wifi_utils_test.cpp | 66 ++++++ .../assoc_frame_bitfields.h | 1 + 5 files changed, 348 insertions(+), 81 deletions(-) create mode 100644 framework/platform/bpl/cfg/uci/bpl_cfg_wifi_utils.h create mode 100644 framework/platform/bpl/test/bpl_cfg_wifi_utils_test.cpp --- a/framework/platform/bpl/cfg/uci/bpl_cfg_wifi.cpp +++ b/framework/platform/bpl/cfg/uci/bpl_cfg_wifi.cpp @@ -14,16 +14,113 @@ #include "bpl_cfg_helper.h" #include "bpl_cfg_uci.h" +#include "bpl_cfg_wifi_utils.h" #include #include #include +#include +#include + using namespace mapf; namespace beerocks { namespace bpl { +static bool same_bss_profile(const son::wireless_utils::sBssInfoConf &lhs, + const son::wireless_utils::sBssInfoConf &rhs) +{ + return lhs.ssid == rhs.ssid && lhs.authentication_type == rhs.authentication_type && + lhs.encryption_type == rhs.encryption_type && lhs.network_key == rhs.network_key && + lhs.fronthaul == rhs.fronthaul && lhs.backhaul == rhs.backhaul; +} + +static void +add_or_merge_bss_profile(std::list &wireless_settings, + son::wireless_utils::sBssInfoConf configuration) +{ + for (auto &existing : wireless_settings) { + if (!same_bss_profile(existing, configuration)) { + continue; + } + existing.operating_class.splice(existing.operating_class.end(), + configuration.operating_class); + existing.operating_class.sort(); + existing.operating_class.unique(); + return; + } + wireless_settings.push_back(std::move(configuration)); +} + +static bool +bpl_cfg_get_explicit_bss_profiles(std::list &wireless_settings) +{ + std::vector sections; + if (!uci_get_all_sections("prplmesh", "bss_profile", sections)) { + LOG(ERROR) << "No explicit prplmesh bss_profile sections found"; + return false; + } + + for (const auto §ion_name : sections) { + OptionsUnorderedMap options; + if (!uci_get_section("prplmesh", "bss_profile", section_name, options)) { + LOG(ERROR) << "Failed to read explicit BSS profile " << section_name; + return false; + } + + son::wireless_utils::sBssInfoConf configuration{}; + configuration.ssid = options["ssid"]; + configuration.network_key = options["key"]; + // An absent encryption option must not silently fall back to an + // open network; intentionally open profiles say 'none' explicitly. + if (options["encryption"].empty()) { + LOG(ERROR) << "Missing encryption in explicit BSS profile " << section_name; + return false; + } + configuration.authentication_type = + wifi_cfg_utils::authentication_from_uci(options["encryption"]); + configuration.encryption_type = wifi_cfg_utils::encryption_from_uci(options["encryption"]); + configuration.fronthaul = wifi_cfg_utils::parse_bool(options["fronthaul"], true); + configuration.backhaul = wifi_cfg_utils::parse_bool(options["backhaul"], false); + + std::istringstream bands(options["bands"]); + std::string band; + while (bands >> band) { + if (!wifi_cfg_utils::append_operating_classes(band, configuration.operating_class)) { + LOG(ERROR) << "Invalid band '" << band << "' in BSS profile " << section_name; + return false; + } + } + + if (configuration.ssid.empty() || configuration.operating_class.empty() || + configuration.authentication_type == WSC::eWscAuth::WSC_AUTH_INVALID || + configuration.encryption_type == WSC::eWscEncr::WSC_ENCR_INVALID) { + LOG(ERROR) << "Incomplete or invalid explicit BSS profile " << section_name; + return false; + } + + if (configuration.authentication_type != WSC::eWscAuth::WSC_AUTH_OPEN) { + const std::string &key = configuration.network_key; + bool valid_key = key.length() >= 8 && key.length() <= 64; + if (valid_key && key.length() == 64) { + // 64 characters mean a raw PSK, which is hexadecimal only; + // passphrases are limited to 8..63 characters. + valid_key = key.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos; + } + if (!valid_key) { + LOG(ERROR) << "Missing or invalid key in secured explicit BSS profile " + << section_name; + return false; + } + } + + add_or_merge_bss_profile(wireless_settings, std::move(configuration)); + } + + return !wireless_settings.empty(); +} + static bool bpl_cfg_get_bss_configuration(const std::string §ion_name, son::wireless_utils::sBssInfoConf &configuration) { @@ -36,38 +133,9 @@ static bool bpl_cfg_get_bss_configuratio // Fill in wireless credentials from option values read from UCI configuration. configuration.ssid = options["ssid"]; - auto starts_with = [](const std::string &prefix, const std::string &value) { - return (value.compare(0, prefix.size(), prefix) == 0); - }; - - auto contains = [](const std::string &substring, const std::string &value) { - return (value.find(substring) != std::string::npos); - }; - - auto get_authentication_type = [&](const std::string &encryption) { - if ("none" == encryption || encryption.empty()) { - return WSC::eWscAuth::WSC_AUTH_OPEN; - } else if (starts_with("psk2", encryption)) { - return WSC::eWscAuth::WSC_AUTH_WPA2PSK; - } else if ("sae" == encryption) { - return WSC::eWscAuth::WSC_AUTH_SAE; - } - return WSC::eWscAuth::WSC_AUTH_INVALID; - }; - configuration.authentication_type = get_authentication_type(options["encryption"]); - - auto get_encryption_type = [&](const std::string &encryption) { - if ("none" == encryption || encryption.empty()) { - return WSC::eWscEncr::WSC_ENCR_NONE; - } else if (contains("+tkip", encryption)) { - return WSC::eWscEncr::WSC_ENCR_TKIP; - } else if (("psk2" == encryption) || ("sae" == encryption) || - contains("+aes", encryption) || contains("+ccmp", encryption)) { - return WSC::eWscEncr::WSC_ENCR_AES; - } - return WSC::eWscEncr::WSC_ENCR_INVALID; - }; - configuration.encryption_type = get_encryption_type(options["encryption"]); + configuration.authentication_type = + wifi_cfg_utils::authentication_from_uci(options["encryption"]); + configuration.encryption_type = wifi_cfg_utils::encryption_from_uci(options["encryption"]); configuration.network_key = options["key"]; @@ -128,8 +196,8 @@ int cfg_get_wifi_params(const std::strin cfg_uci_get_wireless_bool(TYPE_RADIO, iface.c_str(), "disabled", &disabled); wlan_params->enabled = !disabled; - if (cfg_uci_get_wireless_bool(TYPE_RADIO, iface, "sub_band_dfs", &wlan_params->sub_band_dfs) == - RETURN_ERR) { + if (cfg_uci_get_wireless_bool(TYPE_RADIO, iface.c_str(), "sub_band_dfs", + &wlan_params->sub_band_dfs) == RETURN_ERR) { // Failed to find "sub_band_dfs", set to to default value. wlan_params->sub_band_dfs = false; } @@ -143,7 +211,7 @@ int cfg_get_wifi_params(const std::strin // country code char alpha_2[MAX_UCI_BUF_LEN] = {0}; - cfg_uci_get_wireless_from_ifname(TYPE_RADIO, iface, "country", alpha_2); + cfg_uci_get_wireless_from_ifname(TYPE_RADIO, iface.c_str(), "country", alpha_2); wlan_params->country_code[0] = alpha_2[0]; wlan_params->country_code[1] = alpha_2[1]; @@ -153,6 +221,15 @@ int cfg_get_wifi_params(const std::strin bool bpl_cfg_get_wireless_settings(std::list &wireless_settings) { + int use_explicit_profiles = 0; + if (cfg_get_prplmesh_param_int_default("use_explicit_bss_profiles", &use_explicit_profiles, + 0) != RETURN_OK) { + return false; + } + if (use_explicit_profiles != 0) { + return bpl_cfg_get_explicit_bss_profiles(wireless_settings); + } + // Get all "wireless.wifi-iface" section names in UCI configuration const std::string package_name = "wireless"; const std::string section_type = "wifi-iface"; @@ -227,7 +304,7 @@ bool bpl_cfg_get_wireless_settings(std:: continue; } - son::wireless_utils::sBssInfoConf configuration; + son::wireless_utils::sBssInfoConf configuration{}; if (!bpl_cfg_get_bss_configuration(section_name, configuration)) { LOG(DEBUG) << "Failed to get SSID and WiFi credentials from section " << section_name; continue; @@ -251,13 +328,11 @@ bool bpl_cfg_get_wireless_settings(std:: continue; } - // Option "hwmode" in device section selects the wireless protocol to use, possible values - // are 11b, 11g, and 11a. + // Modern OpenWrt uses option "band". Fall back to legacy "hwmode". + std::string band; + uci_get_option(package_name, "wifi-device", device, "band", band); std::string hwmode; - if (!uci_get_option(package_name, "wifi-device", device, "hwmode", hwmode)) { - LOG(DEBUG) << "Failed to get 'hwmode' from section " << device; - continue; - } + uci_get_option(package_name, "wifi-device", device, "hwmode", hwmode); // The mode used by upstream hostapd (11b, 11g, 11n, 11ac, 11ax) is governed by several parameters in // the configuration file. However, as explained in the comment below from hostapd.conf, the @@ -274,17 +349,11 @@ bool bpl_cfg_get_wireless_settings(std:: // // For MaxLinear's devices, by default '11bgnax' is used for 2.4Ghz bands, and '11anacax' is // used for 5Ghz bands (see 'files/scripts/lib/netifd/wireless/mac80211.sh' in the swpal package). - if (hwmode.empty() || (hwmode == "11b") || (hwmode == "11g") || hwmode == "11bgnax") { - configuration.operating_class.splice( - configuration.operating_class.end(), - son::wireless_utils::string_to_wsc_oper_class("24g")); - } else if (hwmode == "11a" || hwmode == "11anacax") { - configuration.operating_class.splice( - configuration.operating_class.end(), - son::wireless_utils::string_to_wsc_oper_class("5g")); - } else { + const auto band_or_hwmode = band.empty() ? hwmode : band; + if (!wifi_cfg_utils::append_operating_classes(band_or_hwmode, + configuration.operating_class)) { LOG(DEBUG) << "Failed to get frequency band for SSID " << configuration.ssid - << " from hwmode " << hwmode; + << " from band/hwmode " << band_or_hwmode; continue; } @@ -326,10 +395,10 @@ bool bpl_cfg_get_wireless_settings(std:: } } - wireless_settings.push_back(configuration); - LOG(DEBUG) << "Configuration added for SSID " << configuration.ssid - << " (hwmode = " << hwmode << ")"; + << " (band/hwmode = " << band_or_hwmode << ")"; + + add_or_merge_bss_profile(wireless_settings, std::move(configuration)); } return true; @@ -340,7 +409,7 @@ bool bpl_cfg_get_wifi_credentials(const { // Find the "wireless.wifi-iface" section in UCI configuration for the given interface std::string section_name; - if (!uci_find_section_by_option("wireless", "wifi-iface", "ifname", iface, section_name)) { + if (!cfg_get_prplmesh_wireless_section(iface, section_name)) { LOG(ERROR) << "Failed to find configuration section for interface " << iface; return false; } @@ -366,9 +435,8 @@ bool bpl_cfg_set_wifi_credentials(const // Find the "wireless.wifi-iface" section in UCI configuration for the given interface const std::string package_name = "wireless"; const std::string section_type = "wifi-iface"; - const std::string option_name = "ifname"; std::string section_name; - if (!uci_find_section_by_option(package_name, section_type, option_name, iface, section_name)) { + if (!cfg_get_prplmesh_wireless_section(iface, section_name)) { LOG(ERROR) << "Failed to find configuration section for interface " << iface; return false; } @@ -382,22 +450,8 @@ bool bpl_cfg_set_wifi_credentials(const OptionsUnorderedMap options; options["ssid"] = configuration.ssid; - auto get_encryption = [](WSC::eWscAuth authentication_type, WSC::eWscEncr encryption_type) { - std::string encryption = "none"; - if (authentication_type == WSC::eWscAuth::WSC_AUTH_WPA2PSK) { - encryption = "psk2"; - if (encryption_type == WSC::eWscEncr::WSC_ENCR_TKIP) { - encryption += "+tkip"; - } else if (encryption_type == WSC::eWscEncr::WSC_ENCR_AES) { - encryption += "+aes"; - } - } else if (authentication_type == WSC::eWscAuth::WSC_AUTH_SAE) { - encryption = "sae"; - } - return encryption; - }; - options["encryption"] = - get_encryption(configuration.authentication_type, configuration.encryption_type); + options["encryption"] = wifi_cfg_utils::encryption_to_uci(configuration.authentication_type, + configuration.encryption_type); options["key"] = configuration.network_key; @@ -476,18 +530,17 @@ void cfg_wifi_reset_wps_credentials() { int cfg_get_hostap_iface(int32_t radio_num, std::string &hostap_iface) { - if (!hostap_iface) { - MAPF_ERR("cfg_get_hostap_iface: invalid input: hostap_iface is NULL"); - return RETURN_ERR; - } - if (radio_num < 0) { MAPF_ERR("cfg_get_hostap_iface: invalid input: radio_num < 0"); return RETURN_ERR; } - char iface_c_str[IFNAMSIZ]; - auto result = cfg_get_prplmesh_radio_param(radio_num, "hostap_iface", c_iface, IFNAMSIZ); + char iface_c_str[IFNAMSIZ] = {0}; + auto result = cfg_get_prplmesh_radio_param(radio_num, "hostap_iface", iface_c_str, IFNAMSIZ); + if (result != RETURN_OK) { + hostap_iface.clear(); + return result; + } hostap_iface = std::string(iface_c_str); return result; } --- /dev/null +++ b/framework/platform/bpl/cfg/uci/bpl_cfg_wifi_utils.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: BSD-2-Clause-Patent */ + +#ifndef BPL_CFG_WIFI_UTILS_H_ +#define BPL_CFG_WIFI_UTILS_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace beerocks { +namespace bpl { +namespace wifi_cfg_utils { + +inline bool starts_with(const std::string &value, const std::string &prefix) +{ + return value.compare(0, prefix.size(), prefix) == 0; +} + +inline bool contains(const std::string &value, const std::string &substring) +{ + return value.find(substring) != std::string::npos; +} + +inline WSC::eWscAuth authentication_from_uci(const std::string &encryption) +{ + if (encryption.empty() || encryption == "none") { + return WSC::eWscAuth::WSC_AUTH_OPEN; + } + if (starts_with(encryption, "sae-mixed") || starts_with(encryption, "psk2+sae")) { + return WSC::eWscAuth(WSC::eWscAuth::WSC_AUTH_WPA2PSK | WSC::eWscAuth::WSC_AUTH_SAE); + } + if (starts_with(encryption, "psk2")) { + return WSC::eWscAuth::WSC_AUTH_WPA2PSK; + } + if (starts_with(encryption, "sae")) { + return WSC::eWscAuth::WSC_AUTH_SAE; + } + return WSC::eWscAuth::WSC_AUTH_INVALID; +} + +inline WSC::eWscEncr encryption_from_uci(const std::string &encryption) +{ + if (encryption.empty() || encryption == "none") { + return WSC::eWscEncr::WSC_ENCR_NONE; + } + if (contains(encryption, "+tkip")) { + return WSC::eWscEncr::WSC_ENCR_TKIP; + } + if (starts_with(encryption, "psk2") || starts_with(encryption, "sae") || + contains(encryption, "+aes") || contains(encryption, "+ccmp")) { + return WSC::eWscEncr::WSC_ENCR_AES; + } + return WSC::eWscEncr::WSC_ENCR_INVALID; +} + +inline std::string encryption_to_uci(WSC::eWscAuth authentication, WSC::eWscEncr encryption) +{ + const auto auth_bits = static_cast(authentication); + if ((auth_bits & WSC::eWscAuth::WSC_AUTH_WPA2PSK) && + (auth_bits & WSC::eWscAuth::WSC_AUTH_SAE)) { + return "sae-mixed"; + } + if (authentication == WSC::eWscAuth::WSC_AUTH_SAE) { + return "sae"; + } + if (authentication == WSC::eWscAuth::WSC_AUTH_WPA2PSK) { + if (encryption == WSC::eWscEncr::WSC_ENCR_TKIP) { + return "psk2+tkip"; + } + return "psk2+aes"; + } + return "none"; +} + +inline bool append_operating_classes(const std::string &band_or_hwmode, + std::list &operating_classes) +{ + std::string value = band_or_hwmode; + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + std::string band; + if (value == "2g" || value == "2.4g" || value == "24g" || value.empty() || value == "11b" || + value == "11g" || value == "11bgnax") { + band = "24g"; + } else if (value == "5g" || value == "5ghz" || value == "11a" || value == "11anacax") { + band = "5g"; + } else if (value == "6g" || value == "6ghz") { + band = "6g"; + } else { + return false; + } + + auto classes = son::wireless_utils::string_to_wsc_oper_class(band); + operating_classes.splice(operating_classes.end(), classes); + operating_classes.sort(); + operating_classes.unique(); + return true; +} + +inline bool parse_bool(const std::string &value, bool default_value) +{ + if (value.empty()) { + return default_value; + } + std::string normalized = value; + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "on") { + return true; + } + if (normalized == "0" || normalized == "false" || normalized == "no" || normalized == "off") { + return false; + } + return default_value; +} + +inline bool select_unique_wireless_section(const std::vector &candidates, + std::string §ion_name) +{ + section_name.clear(); + if (candidates.size() != 1 || candidates.front().empty()) { + return false; + } + section_name = candidates.front(); + return true; +} + +} // namespace wifi_cfg_utils +} // namespace bpl +} // namespace beerocks + +#endif // BPL_CFG_WIFI_UTILS_H_ --- a/framework/platform/bpl/test/CMakeLists.txt +++ b/framework/platform/bpl/test/CMakeLists.txt @@ -5,3 +5,11 @@ target_include_directories(bpl_test PUBL $ $ ) + +add_executable(bpl_cfg_wifi_utils_test bpl_cfg_wifi_utils_test.cpp) +target_link_libraries(bpl_cfg_wifi_utils_test gtest_main bcl) +target_include_directories(bpl_cfg_wifi_utils_test PRIVATE + $ + $ +) +add_test(NAME bpl_cfg_wifi_utils_test COMMAND $) --- /dev/null +++ b/framework/platform/bpl/test/bpl_cfg_wifi_utils_test.cpp @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: BSD-2-Clause-Patent */ + +#include "../cfg/uci/bpl_cfg_wifi_utils.h" + +#include + +using namespace beerocks::bpl::wifi_cfg_utils; + +TEST(BplCfgWifiUtils, MapsStockOpenWrtSecurityModes) +{ + EXPECT_EQ(WSC::eWscAuth::WSC_AUTH_WPA2PSK, authentication_from_uci("psk2")); + EXPECT_EQ(WSC::eWscAuth::WSC_AUTH_SAE, authentication_from_uci("sae")); + const auto transition = authentication_from_uci("sae-mixed"); + EXPECT_NE(0, static_cast(transition) & WSC::eWscAuth::WSC_AUTH_WPA2PSK); + EXPECT_NE(0, static_cast(transition) & WSC::eWscAuth::WSC_AUTH_SAE); + EXPECT_EQ(WSC::eWscEncr::WSC_ENCR_AES, encryption_from_uci("sae-mixed")); +} + +TEST(BplCfgWifiUtils, MapsSecurityBackToUci) +{ + const auto transition = + static_cast(WSC::eWscAuth::WSC_AUTH_WPA2PSK | WSC::eWscAuth::WSC_AUTH_SAE); + EXPECT_EQ("psk2+aes", + encryption_to_uci(WSC::eWscAuth::WSC_AUTH_WPA2PSK, WSC::eWscEncr::WSC_ENCR_AES)); + EXPECT_EQ("sae", encryption_to_uci(WSC::eWscAuth::WSC_AUTH_SAE, WSC::eWscEncr::WSC_ENCR_AES)); + EXPECT_EQ("sae-mixed", encryption_to_uci(transition, WSC::eWscEncr::WSC_ENCR_AES)); +} + +TEST(BplCfgWifiUtils, MergesTwoFourFiveAndSixGhzOperatingClasses) +{ + std::list classes; + EXPECT_TRUE(append_operating_classes("2g", classes)); + EXPECT_TRUE(append_operating_classes("5g", classes)); + EXPECT_TRUE(append_operating_classes("6g", classes)); + EXPECT_NE(classes.end(), std::find(classes.begin(), classes.end(), 81)); + EXPECT_NE(classes.end(), std::find(classes.begin(), classes.end(), 115)); + EXPECT_NE(classes.end(), std::find(classes.begin(), classes.end(), 131)); + + const auto before = classes; + EXPECT_FALSE(append_operating_classes("invalid-band", classes)); + EXPECT_EQ(before, classes); +} + +TEST(BplCfgWifiUtils, ParsesExplicitBooleansWithSafeDefault) +{ + EXPECT_TRUE(parse_bool("yes", false)); + EXPECT_FALSE(parse_bool("off", true)); + EXPECT_TRUE(parse_bool("invalid", true)); +} + +TEST(BplCfgWifiUtils, SelectsOnlyAnUnambiguousWirelessSection) +{ + const std::vector none; + const std::vector one{"default_radio0"}; + const std::vector two{"default_radio0", "guest_radio0"}; + std::string selected; + + EXPECT_FALSE(select_unique_wireless_section(none, selected)); + EXPECT_TRUE(selected.empty()); + + EXPECT_TRUE(select_unique_wireless_section(one, selected)); + EXPECT_EQ("default_radio0", selected); + + EXPECT_FALSE(select_unique_wireless_section(two, selected)); + EXPECT_TRUE(selected.empty()); +} --- a/framework/tlvf/src/include/tlvf/AssociationRequestFrame/assoc_frame_bitfields.h +++ b/framework/tlvf/src/include/tlvf/AssociationRequestFrame/assoc_frame_bitfields.h @@ -4,6 +4,7 @@ #include #include +#include #include namespace assoc_frame {