diff --git a/.github/workflows/multi-arch-test-build.yml b/.github/workflows/multi-arch-test-build.yml index c8c70872221dd..aa45eb4bf830b 100644 --- a/.github/workflows/multi-arch-test-build.yml +++ b/.github/workflows/multi-arch-test-build.yml @@ -9,19 +9,6 @@ permissions: checks: read jobs: - wait-for-formalities: - name: Wait for FormalityCheck - runs-on: ubuntu-slim - steps: - - name: Wait for Check Run - uses: lewagon/wait-on-check-action@v1.8.1 - with: - ref: ${{ github.event.pull_request.head.sha }} - check-name: 'FormalityCheck / Git & Commits' - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 5 - build: name: Feeds Package Test Build - needs: wait-for-formalities uses: openwrt/actions-shared-workflows/.github/workflows/multi-arch-test-build.yml@main diff --git a/net/prplmesh-stock/Makefile b/net/prplmesh-stock/Makefile new file mode 100644 index 0000000000000..0ef4495d88ab4 --- /dev/null +++ b/net/prplmesh-stock/Makefile @@ -0,0 +1,76 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=prplmesh-stock +PKG_VERSION:=6.0.1 +PKG_RELEASE:=1 + +PKG_SOURCE:=prplMesh-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://gitlab.com/prpl-foundation/prplmesh/prplMesh/-/archive/$(PKG_VERSION)/ +PKG_HASH:=68602c7bdf521de1797c0e58e66bb45cf105726dfd5a75d8429038012e19a702 +PKG_BUILD_DIR:=$(BUILD_DIR)/prplMesh-$(PKG_VERSION) + +PKG_LICENSE:=BSD-2-Clause-Patent BSD-3-Clause ISC MIT +PKG_LICENSE_FILES:=LICENSE LICENSES/BSD-3-Clause.txt LICENSES/ISC.txt LICENSES/MIT.txt +PKG_MAINTAINER:=Andrei Ovcharenko + +PKG_BUILD_DEPENDS:=python-yaml/host +CMAKE_BINARY_SUBDIR:=build-openwrt-stock + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk +include ../../lang/python/python3-host.mk + +EXTRA_CXXFLAGS += -Wno-error=deprecated-declarations + +define Package/prplmesh-stock + SECTION:=net + CATEGORY:=Network + TITLE:=prplMesh controller and agent for stock OpenWrt + URL:=https://gitlab.com/prpl-foundation/prplmesh/prplMesh + DEPENDS:=+libstdcpp +libpthread +librt +libjson-c +libopenssl +libnl-core +libnl-genl +libnl-route +libuci +libubus +libubox +endef + +define Package/prplmesh-stock/description + prplMesh EasyMesh controller and agent built for stock OpenWrt. It talks + to the hostapd instances of the installed wpad over their UNIX control + sockets through a small bundled control-interface client; it does not + bundle, build or replace hostapd. +endef + +define Package/prplmesh-stock/conffiles +/etc/config/prplmesh +endef + +CMAKE_OPTIONS += \ + -DTARGET_PLATFORM=openwrt \ + -DTARGET_PLATFORM_TYPE=stock \ + -DBWL_TYPE=NL80211 \ + -DBWL_BUNDLED_WPA_CTRL=ON \ + -DENABLE_NBAPI=OFF \ + -DUSE_PRPLMESH_WHM=OFF \ + -DPRPLMESH_STOCK_OPENWRT_UCI=ON \ + -DBUILD_AGENT=ON \ + -DBUILD_CONTROLLER=ON \ + -DBUILD_TESTS=OFF \ + -DPRPLMESH_VERSION=$(PKG_VERSION) \ + -DPRPLMESH_REVISION=openwrt-r$(PKG_RELEASE) \ + -DBUILD_DATE=$(SOURCE_DATE_EPOCH) \ + -DCMAKE_INSTALL_PREFIX=/usr/libexec/prplmesh \ + -DPLATFORM_STAGING_DIR=$(STAGING_DIR) \ + -DPLATFORM_INCLUDE_DIR=$(STAGING_DIR)/usr/include \ + -DPython3_EXECUTABLE=$(HOST_PYTHON3_BIN) + +define Package/prplmesh-stock/install + $(INSTALL_DIR) $(1)/usr/libexec/prplmesh $(1)/usr/share/prplmesh $(1)/usr/lib $(1)/etc/init.d $(1)/etc/config + $(CP) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/bin $(1)/usr/libexec/prplmesh/ + $(CP) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/scripts $(1)/usr/libexec/prplmesh/ + $(CP) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/share $(1)/usr/share/prplmesh/ + $(CP) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/config $(1)/usr/share/prplmesh/ + $(LN) ../../share/prplmesh/share $(1)/usr/libexec/prplmesh/share + $(LN) ../../share/prplmesh/config $(1)/usr/libexec/prplmesh/config + $(CP) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/lib/*.so* $(1)/usr/lib/ + $(INSTALL_BIN) ./files/prplmesh.init $(1)/etc/init.d/prplmesh + $(INSTALL_CONF) ./files/prplmesh.config $(1)/etc/config/prplmesh +endef + +$(eval $(call BuildPackage,prplmesh-stock)) diff --git a/net/prplmesh-stock/files/prplmesh.config b/net/prplmesh-stock/files/prplmesh.config new file mode 100644 index 0000000000000..c0262f626ea0d --- /dev/null +++ b/net/prplmesh-stock/files/prplmesh.config @@ -0,0 +1,68 @@ +# beerocks derives radio and VAP identifiers from wlan interface names, +# so the fronthaul BSS netdevs must be named wlan (wireless section +# 'option ifname'). Adjust the interface lists below to the actual names. + +config prplmesh 'config' + option enabled '0' + option management_mode 'Multi-AP-Controller-and-Agent' + option enable '1' + option onboarding '0' + option master '1' + option gateway '1' + option passive_mode '0' + option wired_backhaul '1' + option operational '0' + option ssid 'REPLACE_WITH_SSID' + option mode_enabled 'WPA2-Personal' + option key_passphrase 'REPLACE_WITH_A_STRONG_PASSWORD' + option mem_only_psk '0' + option certification_mode '0' + option stop_on_failure_attempts '0' + option backhaul_wire_iface 'br-lan' + option backhaul_band 'auto' + option mandatory_interfaces 'wlan0,wlan1' + option band_steering '1' + option client_11k_roaming '1' + option client_roaming '1' + option load_balancing '0' + option dfs_reentry '1' + option roaming_hysteresis_percent_bonus '10' + option steering_disassoc_timer_msec '200' + option link_metrics_request_interval_sec '60' + option clients_measurement_mode '1' + option radio_stats_enable '1' + option unsuccessful_assoc_report_policy '1' + option unsuccessful_assoc_max_reporting_rate '30' + option ire_roaming '1' + option exclude_hostap_iface '0' + option use_explicit_bss_profiles '1' + +config wifi-device 'radio0' + option hostap_iface 'wlan0' + option wireless_section 'default_radio0' + option wireless_device 'radio0' + option hostap_iface_steer_vaps 'wlan0' + option dcs_enable '0' + +config wifi-device 'radio1' + option hostap_iface 'wlan1' + option wireless_section 'default_radio1' + option wireless_device 'radio1' + option hostap_iface_steer_vaps 'wlan1' + option dcs_enable '0' + +config bss_profile 'main_24_5' + option ssid 'REPLACE_WITH_SSID' + option key 'REPLACE_WITH_A_STRONG_PASSWORD' + option encryption 'sae-mixed' + option bands '2g 5g' + option fronthaul '1' + option backhaul '0' + +config bss_profile 'main_6' + option ssid 'REPLACE_WITH_SSID' + option key 'REPLACE_WITH_A_STRONG_PASSWORD' + option encryption 'sae' + option bands '6g' + option fronthaul '1' + option backhaul '0' diff --git a/net/prplmesh-stock/files/prplmesh.init b/net/prplmesh-stock/files/prplmesh.init new file mode 100644 index 0000000000000..901bb4ba3174b --- /dev/null +++ b/net/prplmesh-stock/files/prplmesh.init @@ -0,0 +1,77 @@ +#!/bin/sh /etc/rc.common +# shellcheck disable=SC2034 +# shellcheck disable=SC3043 # ash supports local + +START=99 +STOP=10 +USE_PROCD=1 + +PRPLMESH_DIR=/usr/libexec/prplmesh + +enabled() { + [ "$(uci -q get prplmesh.config.enabled)" = "1" ] +} + +register_process() { + local name="$1"; shift + procd_open_instance "$name" + procd_set_param command "$@" + procd_set_param env PRPLMESH_DIR="$PRPLMESH_DIR" + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +start_service() { + local mode need_agent=1 need_controller=1 + + enabled || return 0 + + [ -x "$PRPLMESH_DIR/bin/ieee1905_transport" ] || return 1 + + # A standalone controller runs without local radios, a plain agent + # without the controller; everything else keeps both. + mode="$(uci -q get prplmesh.config.management_mode)" + case "$mode" in + Multi-AP-Controller) need_agent=0 ;; + Multi-AP-Agent) need_controller=0 ;; + esac + + register_process transport "$PRPLMESH_DIR/bin/ieee1905_transport" + + if [ "$need_controller" = 1 ]; then + [ -x "$PRPLMESH_DIR/bin/beerocks_controller" ] || return 1 + register_process controller "$PRPLMESH_DIR/bin/beerocks_controller" + fi + + if [ "$need_agent" = 1 ]; then + [ -x "$PRPLMESH_DIR/bin/beerocks_agent" ] || return 1 + [ -n "$(uci -q get prplmesh.config.mandatory_interfaces)" ] || { + logger -t prplmesh "mandatory_interfaces not set; agent left stopped" + return 1 + } + # hostapd recreates its control sockets on boot and on wireless + # reloads, so wait for them under procd supervision instead of + # failing the whole start on a transient race. + # shellcheck disable=SC2016 + register_process agent /bin/sh -c ' + while :; do + ok=1 + IFS="," + for interface in $(uci -q get prplmesh.config.mandatory_interfaces); do + unset IFS + [ -n "$interface" ] || continue + [ -S "/var/run/hostapd/$interface" ] || { ok=0; break; } + done + unset IFS + [ "$ok" = 1 ] && break + sleep 5 + done + exec "$PRPLMESH_DIR/bin/beerocks_agent"' + fi +} + +service_triggers() { + procd_add_reload_trigger prplmesh wireless network +} diff --git a/net/prplmesh-stock/patches/100-stock-platform.patch b/net/prplmesh-stock/patches/100-stock-platform.patch new file mode 100644 index 0000000000000..f352b1d785a95 --- /dev/null +++ b/net/prplmesh-stock/patches/100-stock-platform.patch @@ -0,0 +1,521 @@ +From 77f2a8f29088e1c2af45b64a4158c57e45b1108a Mon Sep 17 00:00:00 2001 +From: Andrei Ovcharenko +Date: Tue, 21 Jul 2026 22:33:14 +0300 +Subject: [PATCH 1/2] platform: support stock OpenWrt UCI and nl80211 + +Add an opt-in platform mode that keeps stock OpenWrt in charge of UCI, +ubus, the Linux bridge and hostapd. In this mode prplMesh uses the +existing nl80211 and hostapd control interfaces instead of depending on +prplOS WHM or Ambiorix. + +The UCI backend reads the runtime bridge and radio mappings, avoids NBAPI +when it is disabled, and supplies the small portability fixes needed by +the stock OpenWrt toolchain. + +Signed-off-by: Andrei Ovcharenko +--- + CMakeLists.txt | 1 + + agent/src/beerocks/slave/CMakeLists.txt | 3 +- + .../beerocks/slave/beerocks_agent_main.cpp | 14 +++ + .../include/bcl/beerocks_ucc_server_factory.h | 1 + + common/beerocks/bwl/shared/nl80211_socket.h | 2 + + controller/config/CMakeLists.txt | 2 +- + .../beerocks/master/beerocks_master_main.cpp | 31 ++++++ + framework/platform/bpl/CMakeLists.txt | 61 +++++++----- + .../bpl/cfg/linux/bpl_cfg_ifaces.cpp.in | 1 + + framework/platform/bpl/cfg/uci/bpl.cpp | 1 + + framework/platform/bpl/cfg/uci/bpl_cfg.cpp | 96 ++++++++++++++++++- + .../platform/bpl/cfg/uci/bpl_cfg_helper.cpp | 47 +++++++++ + .../platform/bpl/cfg/uci/bpl_cfg_helper.h | 10 ++ + .../platform/bpl/cfg/uci/bpl_cfg_uci.cpp | 18 ++++ + 14 files changed, 261 insertions(+), 27 deletions(-) + +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -66,6 +66,7 @@ option (ENABLE_NBAPI "Build the northbou + option (ENABLE_VBSS "Build with Virtual BSS support" ON) + + option(USE_PRPLMESH_WHM "Use prplMesh Wireless Hardware Manager" ON) ++option(PRPLMESH_STOCK_OPENWRT_UCI "Build against stock OpenWrt UCI/ubus without Ambiorix or WHM" OFF) + + ## Generic checks and defaults + +--- a/agent/src/beerocks/slave/CMakeLists.txt ++++ b/agent/src/beerocks/slave/CMakeLists.txt +@@ -15,11 +15,12 @@ file(GLOB beerocks_agent_sources + ${MODULE_PATH}/tasks/*.c* + ${MODULE_PATH}/traffic_separation/*.c* + ${MODULE_PATH}/gate/*.c* +- ${MODULE_PATH}/nbapi/*.c* + ${MODULE_PATH}/*.c* + ) + + if (ENABLE_NBAPI) ++ file(GLOB beerocks_agent_nbapi_sources ${MODULE_PATH}/nbapi/*.c*) ++ list(APPEND beerocks_agent_sources ${beerocks_agent_nbapi_sources}) + add_subdirectory("nbapi") + endif() + +--- a/agent/src/beerocks/slave/beerocks_agent_main.cpp ++++ b/agent/src/beerocks/slave/beerocks_agent_main.cpp +@@ -632,6 +632,20 @@ int main(int argc, char *argv[]) + return 1; + } + } ++ ++ // Stock OpenWrt can move the wired EasyMesh backhaul between bridges at ++ // runtime (for example from the management LAN to a dedicated SSID ++ // network). Keep every agent component on the UCI-selected bridge rather ++ // than the static value shipped in beerocks_agent.conf. ++ std::string configured_bridge_iface; ++ if (beerocks::bpl::bpl_cfg_get_backhaul_wire_iface(configured_bridge_iface) && ++ !configured_bridge_iface.empty()) { ++ beerocks_slave_conf.bridge_iface = configured_bridge_iface; ++ } else { ++ LOG(WARNING) << "Unable to read backhaul_wire_iface; using bridge_iface='" ++ << beerocks_slave_conf.bridge_iface << "' from agent configuration"; ++ } ++ + // beerocks system hang tester + if (beerocks_slave_conf.enable_system_hang_test == "1") { + +--- a/common/beerocks/bcl/include/bcl/beerocks_ucc_server_factory.h ++++ b/common/beerocks/bcl/include/bcl/beerocks_ucc_server_factory.h +@@ -12,6 +12,7 @@ + #include + #include + ++#include + #include + + namespace beerocks { +--- a/common/beerocks/bwl/shared/nl80211_socket.h ++++ b/common/beerocks/bwl/shared/nl80211_socket.h +@@ -21,6 +21,8 @@ namespace bwl { + class nl80211_socket : public nl_genl_socket { + + public: ++ using nl_genl_socket::send_receive_msg; ++ + /** + * @brief Class destructor. + */ +--- a/controller/config/CMakeLists.txt ++++ b/controller/config/CMakeLists.txt +@@ -1,5 +1,5 @@ + set(BEEROCKS_CONTROLLER_DATAELEMENTS_VAP_CONFIGS 0) +-if (NOT USE_PRPLMESH_WHM) ++if (NOT USE_PRPLMESH_WHM AND NOT PRPLMESH_STOCK_OPENWRT_UCI) + set(BEEROCKS_CONTROLLER_DATAELEMENTS_VAP_CONFIGS 1) + endif() + +--- a/controller/src/beerocks/master/beerocks_master_main.cpp ++++ b/controller/src/beerocks/master/beerocks_master_main.cpp +@@ -672,6 +672,18 @@ int main(int argc, char *argv[]) + } + } + ++ // The wired EasyMesh tree can be moved between OpenWrt bridges at runtime. ++ // Keep the controller on the same UCI-selected bridge as the agent instead ++ // of retaining the static bridge_iface from beerocks_agent.conf. ++ std::string configured_bridge_iface; ++ if (beerocks::bpl::bpl_cfg_get_backhaul_wire_iface(configured_bridge_iface) && ++ !configured_bridge_iface.empty()) { ++ beerocks_slave_conf.bridge_iface = configured_bridge_iface; ++ } else { ++ LOG(WARNING) << "Unable to read backhaul_wire_iface; using bridge_iface='" ++ << beerocks_slave_conf.bridge_iface << "' from agent configuration"; ++ } ++ + std::string base_master_name = std::string(BEEROCKS_CONTROLLER); + + //kill running master +@@ -820,6 +832,25 @@ int main(int argc, char *argv[]) + beerocks::btl::create_broker_client_factory(broker_uds_path, event_loop); + LOG_IF(!broker_client_factory, FATAL) << "Unable to create broker client factory!"; + ++ // The controller can run without a local agent. Configure the transport directly so that ++ // IEEE1905.1 frames received on the bridge are not dropped while no agent is present (or while ++ // the local agent is still starting). Keep this client alive for the lifetime of the controller. ++ auto transport_config_client = broker_client_factory->create_instance(); ++ if (!transport_config_client) { ++ LOG(ERROR) << "Unable to create broker client for transport configuration!"; ++ return 1; ++ } ++ ++ if (!transport_config_client->configure_al_mac(tlvf::mac_from_string(bridge_info.mac))) { ++ LOG(ERROR) << "Unable to configure transport AL MAC " << bridge_info.mac; ++ return 1; ++ } ++ ++ if (!transport_config_client->configure_interfaces(bridge_iface, {}, true, true)) { ++ LOG(ERROR) << "Unable to configure transport bridge " << bridge_iface; ++ return 1; ++ } ++ + son::Controller controller(master_db, std::move(broker_client_factory), std::move(ucc_server), + std::move(cmdu_server), timer_manager, event_loop); + +--- a/framework/platform/bpl/CMakeLists.txt ++++ b/framework/platform/bpl/CMakeLists.txt +@@ -87,19 +87,32 @@ if (TARGET_PLATFORM STREQUAL "openwrt") + add_definitions(-DHAVE_C99) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99") + +- # Other OpenWRT based platforms (e.g. Turris-Omnia or GL.iNet-B1300) ++ # Stock OpenWrt with native UCI/ubus and no Ambiorix/WHM dependencies. + else() + +- file(GLOB_RECURSE bpl_platform_sources +- ${MODULE_PATH}/arp/linux/*.c* +- ${MODULE_PATH}/dhcp/luci_rpc/*.c* +- ${MODULE_PATH}/db/uci/*.c* +- ${MODULE_PATH}/cfg/dm/*.c* +- ${MODULE_PATH}/cfg/vendor/*.c* +- ${MODULE_PATH}/common/uci/*.c* +- ${MODULE_PATH}/board/system_ubus/*.c* +- ${MODULE_PATH}/service_prioritization/tc/service_prio_utils_tc.cpp +- ) ++ if (PRPLMESH_STOCK_OPENWRT_UCI) ++ file(GLOB_RECURSE bpl_platform_sources ++ ${MODULE_PATH}/arp/linux/*.c* ++ ${MODULE_PATH}/cfg/uci/*.c* ++ ${MODULE_PATH}/cfg/vendor/*.c* ++ ${MODULE_PATH}/common/uci/*.c* ++ ${MODULE_PATH}/db/uci/*.c* ++ ${MODULE_PATH}/dhcp/uci/*.c* ++ ${MODULE_PATH}/board/system_ubus/*.c* ++ ${MODULE_PATH}/service_prioritization/tc/service_prio_utils_tc.cpp ++ ) ++ else() ++ file(GLOB_RECURSE bpl_platform_sources ++ ${MODULE_PATH}/arp/linux/*.c* ++ ${MODULE_PATH}/dhcp/luci_rpc/*.c* ++ ${MODULE_PATH}/db/uci/*.c* ++ ${MODULE_PATH}/cfg/dm/*.c* ++ ${MODULE_PATH}/cfg/vendor/*.c* ++ ${MODULE_PATH}/common/uci/*.c* ++ ${MODULE_PATH}/board/system_ubus/*.c* ++ ${MODULE_PATH}/service_prioritization/tc/service_prio_utils_tc.cpp ++ ) ++ endif() + + find_package(ubus REQUIRED) + +@@ -159,11 +172,13 @@ else() + + endif() + +-find_package(amxb REQUIRED) +-find_package(amxc REQUIRED) +-find_package(amxd REQUIRED) +-find_package(amxp REQUIRED) +-list(APPEND BPL_LIBS amxb amxc amxd amxp) ++if (NOT PRPLMESH_STOCK_OPENWRT_UCI) ++ find_package(amxb REQUIRED) ++ find_package(amxc REQUIRED) ++ find_package(amxd REQUIRED) ++ find_package(amxp REQUIRED) ++ list(APPEND BPL_LIBS amxb amxc amxd amxp) ++endif() + + # BPL pwhm DM via wbapi + if (USE_PRPLMESH_WHM) +@@ -185,7 +200,7 @@ if (USE_PRPLMESH_WHM) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WBAPI_SOCK}") + + list(APPEND BPL_LIBS wbapi) +-else () ++elseif (NOT PRPLMESH_STOCK_OPENWRT_UCI) + file(GLOB_RECURSE bpl_platform_sources + ${MODULE_PATH}/*/linux/*.c* + ${MODULE_PATH}/service_prioritization/dummy/service_prio_utils_dummy.c* +@@ -194,11 +209,13 @@ else () + endif() + + # BPL configuration via nbapi +-list(FIND bpl_platform_sources ${MODULE_PATH}/cfg/dm/bpl_amx.cpp _index) +-message("bpl_platform_sources: ${bpl_platform_sources}") +-if (_index EQUAL -1) +- message("APPEND bpl_platform_sources with ${MODULE_PATH}/cfg/dm/bpl_amx.cpp") +- list(APPEND bpl_platform_sources ${MODULE_PATH}/cfg/dm/bpl_amx.cpp) ++if (NOT PRPLMESH_STOCK_OPENWRT_UCI) ++ list(FIND bpl_platform_sources ${MODULE_PATH}/cfg/dm/bpl_amx.cpp _index) ++ message("bpl_platform_sources: ${bpl_platform_sources}") ++ if (_index EQUAL -1) ++ message("APPEND bpl_platform_sources with ${MODULE_PATH}/cfg/dm/bpl_amx.cpp") ++ list(APPEND bpl_platform_sources ${MODULE_PATH}/cfg/dm/bpl_amx.cpp) ++ endif() + endif() + list(APPEND BPL_LIBS nbapi) + +--- a/framework/platform/bpl/cfg/linux/bpl_cfg_ifaces.cpp.in ++++ b/framework/platform/bpl/cfg/linux/bpl_cfg_ifaces.cpp.in +@@ -6,6 +6,7 @@ + * See LICENSE file for more details. + */ + ++ #include + #include + + namespace beerocks { +--- a/framework/platform/bpl/cfg/uci/bpl.cpp ++++ b/framework/platform/bpl/cfg/uci/bpl.cpp +@@ -7,6 +7,7 @@ + */ + + #include ++#include + + #include + +--- a/framework/platform/bpl/cfg/uci/bpl_cfg.cpp ++++ b/framework/platform/bpl/cfg/uci/bpl_cfg.cpp +@@ -16,14 +16,41 @@ + #include "bpl_cfg_uci.h" + + #include ++#include + #include + #include + ++#include ++ + using namespace mapf; + + namespace beerocks { + namespace bpl { + ++static bool read_device_info_file(const char *path, std::string &value) ++{ ++ std::ifstream input(path, std::ios::binary); ++ if (!input.good()) { ++ return false; ++ } ++ ++ std::getline(input, value, '\0'); ++ auto line_end = value.find_first_of("\r\n"); ++ if (line_end != std::string::npos) { ++ value.erase(line_end); ++ } ++ while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { ++ value.pop_back(); ++ } ++ auto first = value.find_first_not_of(" \t"); ++ if (first == std::string::npos) { ++ value.clear(); ++ return false; ++ } ++ value.erase(0, first); ++ return true; ++} ++ + int cfg_get_hostap_iface_steer_vaps(int32_t radio_num, + char hostap_iface_steer_vaps[BPL_LOAD_STEER_ON_VAPS_LEN]) + { +@@ -152,8 +179,8 @@ int cfg_get_dcs_channel_pool(const BPL_W + return RETURN_ERR; + } + +- if (radio_num < 0) { +- MAPF_ERR("invalid input: radio_num < 0"); ++ if (iface.radio_num < 0) { ++ MAPF_ERR("invalid input: iface.radio_num < 0"); + return RETURN_ERR; + } + +@@ -1102,8 +1129,12 @@ bool cfg_get_clients_unicast_measurement + + bool cfg_get_private_bridge_iface(std::string &bridge_iface) + { ++ if (bpl_cfg_get_backhaul_wire_iface(bridge_iface) && !bridge_iface.empty()) { ++ return true; ++ } ++ + bridge_iface = DEFAULT_PRIVATE_BRIDGE_IFACE; +- return true; ++ return false; + } + + bool cfg_get_guest_bridge_iface(std::string &bridge_iface) +@@ -1132,5 +1163,64 @@ bool cfg_get_traffic_separation_guest_vi + + bool cfg_commit_changes() { return uci_commit_changes("prplmesh"); } + ++bool get_serial_number(std::string &serial_number) ++{ ++ if (!read_device_info_file("/sys/firmware/devicetree/base/serial-number", serial_number) && ++ !read_device_info_file("/proc/device-tree/serial-number", serial_number) && ++ !read_device_info_file("/tmp/sysinfo/board_name", serial_number)) { ++ serial_number = "openwrt-prplmesh"; ++ } ++ return true; ++} ++ ++bool get_software_version(std::string &software_version) ++{ ++ software_version = beerocks::version::get_module_version(); ++ return true; ++} ++ ++bool get_manufacturer(std::string &manufacturer) ++{ ++ if (!read_device_info_file("/tmp/sysinfo/board_name", manufacturer)) { ++ manufacturer = "OpenWrt"; ++ return true; ++ } ++ ++ auto separator = manufacturer.find(','); ++ if (separator != std::string::npos) { ++ manufacturer.erase(separator); ++ } ++ return true; ++} ++ ++bool get_model_name(std::string &model_name) ++{ ++ if (!read_device_info_file("/tmp/sysinfo/model", model_name)) { ++ model_name = "OpenWrt"; ++ } ++ return true; ++} ++ ++bool get_model_number(std::string &model_number) ++{ ++ if (!read_device_info_file("/tmp/sysinfo/board_name", model_number)) { ++ return get_model_name(model_number); ++ } ++ return true; ++} ++ ++bool get_ruid_chipset_vendor(const sMacAddr &ruid, std::string &chipset_vendor) ++{ ++ (void)ruid; ++ chipset_vendor = "generic-nl80211"; ++ return true; ++} ++ ++bool get_max_prioritization_rules(uint32_t &max_prioritization_rules) ++{ ++ max_prioritization_rules = 1; ++ return true; ++} ++ + } // namespace bpl + } // namespace beerocks +--- a/framework/platform/bpl/cfg/uci/bpl_cfg_helper.cpp ++++ b/framework/platform/bpl/cfg/uci/bpl_cfg_helper.cpp +@@ -11,6 +11,7 @@ + + #include "bpl_cfg_helper.h" + #include "bpl_cfg_uci.h" ++#include "bpl_cfg_wifi_utils.h" + + #include + #include +@@ -38,6 +39,52 @@ int cfg_get_prplmesh_hostapd_ifaces(std: + (char *)"hostap_iface", hostapd_ifaces); + } + ++bool cfg_get_prplmesh_wireless_section(const std::string &iface, std::string §ion_name) ++{ ++ section_name.clear(); ++ ++ // Keep compatibility with systems that still explicitly configure ifname. ++ if (uci_find_section_by_option("wireless", "wifi-iface", "ifname", iface, section_name) && ++ !section_name.empty()) { ++ return true; ++ } ++ ++ std::string prplmesh_radio; ++ if (!uci_find_section_by_option("prplmesh", "wifi-device", "hostap_iface", iface, ++ prplmesh_radio) || ++ prplmesh_radio.empty()) { ++ return false; ++ } ++ ++ if (uci_get_option("prplmesh", "wifi-device", prplmesh_radio, "wireless_section", ++ section_name) && ++ uci_section_exists("wireless", "wifi-iface", section_name)) { ++ return true; ++ } ++ ++ std::string wireless_device; ++ if (!uci_get_option("prplmesh", "wifi-device", prplmesh_radio, "wireless_device", ++ wireless_device)) { ++ wireless_device = prplmesh_radio; ++ } ++ ++ std::vector sections; ++ if (!uci_get_all_sections("wireless", "wifi-iface", sections)) { ++ return false; ++ } ++ std::vector matching_sections; ++ for (const auto &candidate : sections) { ++ std::string device; ++ std::string mode; ++ if (uci_get_option("wireless", "wifi-iface", candidate, "device", device) && ++ device == wireless_device && ++ uci_get_option("wireless", "wifi-iface", candidate, "mode", mode) && mode == "ap") { ++ matching_sections.push_back(candidate); ++ } ++ } ++ return wifi_cfg_utils::select_unique_wireless_section(matching_sections, section_name); ++} ++ + int cfg_get_prplmesh_radio_param(int radio_id, const std::string &radio_param, char *buf, + size_t buf_len) + { +--- a/framework/platform/bpl/cfg/uci/bpl_cfg_helper.h ++++ b/framework/platform/bpl/cfg/uci/bpl_cfg_helper.h +@@ -26,6 +26,16 @@ namespace bpl { + int cfg_get_prplmesh_hostapd_ifaces(std::unordered_map &hostapd_ifaces); + + /** ++ * Resolve a runtime hostapd interface to its stock OpenWrt wifi-iface section. ++ * ++ * Modern OpenWrt normally omits the legacy wireless.*.ifname option. In that ++ * case the prplmesh wifi-device section may provide wireless_section (for ++ * example default_radio0). As a compatibility fallback, wireless_device or ++ * the prplmesh section name is matched against wireless.*.device. ++ */ ++bool cfg_get_prplmesh_wireless_section(const std::string &iface, std::string §ion_name); ++ ++/** + * Returns the value of requested param from DB + * + * @param [in] param prplmesh param key string +--- a/framework/platform/bpl/cfg/uci/bpl_cfg_uci.cpp ++++ b/framework/platform/bpl/cfg/uci/bpl_cfg_uci.cpp +@@ -7,6 +7,7 @@ + */ + + #include "bpl_cfg_uci.h" ++#include "bpl_cfg_helper.h" + #include "../../common/utils/utils.h" + #include + #include +@@ -130,6 +131,23 @@ int cfg_uci_get_wireless_from_ifname(enu + } + } + ++ // Modern stock OpenWrt omits wireless.*.ifname. Resolve the runtime ++ // hostapd interface through the explicit prplmesh mapping first. ++ if (!is_section_found) { ++ std::string mapped_section; ++ if (cfg_get_prplmesh_wireless_section(interface_name, mapped_section)) { ++ uci_foreach_element(&p->sections, e) ++ { ++ s = uci_to_section(e); ++ if (!strncmp(s->type, "wifi-iface", MAX_UCI_BUF_LEN) && ++ mapped_section == s->e.name) { ++ is_section_found = true; ++ break; ++ } ++ } ++ } ++ } ++ + // If interface not found in etc/config/wireless 'config wifi-iface'. + // Try to get ifname using option path from 'config wifi-device'. + // This doesn't work for RDK-B, since there is no etc/config/wireless file. diff --git a/net/prplmesh-stock/patches/110-stock-wifi-hardening.patch b/net/prplmesh-stock/patches/110-stock-wifi-hardening.patch new file mode 100644 index 0000000000000..0c029ee5c230b --- /dev/null +++ b/net/prplmesh-stock/patches/110-stock-wifi-hardening.patch @@ -0,0 +1,583 @@ +From 90a541b420a5461adaaa102d8583de97aeebc855 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: psk2/sae +profiles without an 8..64 character network key are rejected. + +Also avoid building platform tests in non-test builds and make the +association-frame bitfields portable across the OpenWrt toolchain. +--- + .../platform/bpl/cfg/uci/bpl_cfg_wifi.cpp | 205 +++++++++++------- + .../platform/bpl/cfg/uci/bpl_cfg_wifi_utils.h | 144 ++++++++++++ + framework/platform/bpl/test/CMakeLists.txt | 8 + + .../bpl/test/bpl_cfg_wifi_utils_test.cpp | 69 ++++++ + .../assoc_frame_bitfields.h | 1 + + 5 files changed, 346 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,102 @@ + + #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"]; ++ 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 && ++ (configuration.network_key.length() < 8 || ++ configuration.network_key.length() > 64)) { ++ 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 +122,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 +185,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 +200,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 +210,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 +293,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 +317,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 +338,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 +384,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 +398,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 +424,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 +439,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 +519,18 @@ 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,144 @@ ++/* 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,69 @@ ++/* SPDX-License-Identifier: BSD-2-Clause-Patent */ ++ ++#include "../cfg/uci/bpl_cfg_wifi_utils.h" ++ ++#include ++ ++namespace { ++ ++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) ++{ ++ 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(WSC::eWscAuth(WSC::eWscAuth::WSC_AUTH_WPA2PSK | ++ WSC::eWscAuth::WSC_AUTH_SAE), ++ 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) ++{ ++ std::string selected; ++ EXPECT_FALSE(select_unique_wireless_section({}, selected)); ++ EXPECT_TRUE(selected.empty()); ++ ++ EXPECT_TRUE(select_unique_wireless_section({"default_radio0"}, selected)); ++ EXPECT_EQ("default_radio0", selected); ++ ++ EXPECT_FALSE( ++ select_unique_wireless_section({"default_radio0", "guest_radio0"}, selected)); ++ EXPECT_TRUE(selected.empty()); ++} ++ ++} // namespace +--- 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 { diff --git a/net/prplmesh-stock/patches/120-bwl-bundled-wpa-ctrl-client.patch b/net/prplmesh-stock/patches/120-bwl-bundled-wpa-ctrl-client.patch new file mode 100644 index 0000000000000..75b6786b8b89b --- /dev/null +++ b/net/prplmesh-stock/patches/120-bwl-bundled-wpa-ctrl-client.patch @@ -0,0 +1,452 @@ +From ecae575f0456f5f15d59032649e569a511bba649 Mon Sep 17 00:00:00 2001 +From: Andrei Ovcharenko +Date: Sun, 26 Jul 2026 21:23:07 +0300 +Subject: [PATCH] bwl: add bundled minimal wpa_ctrl client + +The NL80211 flavor compiles wpa_ctrl.c and os_unix.c straight out of a +hostapd source tree, so every integration has to carry a hostapd +checkout at build time even when the target only talks to the control +sockets of an already installed hostapd. + +Add a small self-contained implementation of the documented control +interface protocol with the same API and semantics as hostap's +wpa_ctrl.h (solicited replies with a 10 s timeout and -2 on expiry, +event filtering while waiting, ATTACH/DETACH, pending/recv/get_fd), and +a BWL_BUNDLED_WPA_CTRL option (default OFF) that builds it instead of +the hostapd sources. The client socket is created group accessible for +user/group network so a hostapd jailed the way OpenWrt does it can +reply, which is what the OpenWrt tree patches into wpa_ctrl.c. + +Signed-off-by: Andrei Ovcharenko +--- + common/beerocks/bwl/CMakeLists.txt | 64 +++-- + .../bwl/nl80211/wpa_ctrl_compat/wpa_ctrl.h | 68 +++++ + .../nl80211/wpa_ctrl_compat/wpa_ctrl_compat.c | 267 ++++++++++++++++++ + 3 files changed, 378 insertions(+), 21 deletions(-) + create mode 100644 common/beerocks/bwl/nl80211/wpa_ctrl_compat/wpa_ctrl.h + create mode 100644 common/beerocks/bwl/nl80211/wpa_ctrl_compat/wpa_ctrl_compat.c + +--- a/common/beerocks/bwl/CMakeLists.txt ++++ b/common/beerocks/bwl/CMakeLists.txt +@@ -115,34 +115,56 @@ elseif(BWL_TYPE STREQUAL "DWPAL") + + elseif(BWL_TYPE STREQUAL "NL80211") + +- file(GLOB HOSTAPD_SEARCH_PATHS "${PLATFORM_BUILD_DIR}/hostapd*/hostapd-*") +- find_path(HOSTAPD_INCLUDE_DIR NAMES "src/common/wpa_ctrl.h" PATHS ${HOSTAPD_SEARCH_PATHS} NO_CMAKE_FIND_ROOT_PATH) +- set(HOSTAPD_DIR "${HOSTAPD_INCLUDE_DIR}") ++ option(BWL_BUNDLED_WPA_CTRL ++ "Use the bundled minimal hostapd control interface client instead of a hostapd source tree" ++ OFF) + + find_package(nl-genl-3 REQUIRED) + list(APPEND BWL_LIBS nl-genl-3) + +- file(GLOB bwl_platform_sources +- ${MODULE_PATH}/shared/*.c* +- ${MODULE_PATH}/nl80211/*.c* +- +- # WPA Control Interface +- ${HOSTAPD_DIR}/src/common/wpa_ctrl.c +- ${HOSTAPD_DIR}/src/utils/os_unix.c +- ) +- +- # Enable WPA control interface +- add_definitions( +- -DCONFIG_CTRL_IFACE +- -DCONFIG_CTRL_IFACE_UNIX +- ) +- +- # Hostapd/NL80211 include directories +- include_directories( +- ${HOSTAPD_DIR}/src/utils +- ${HOSTAPD_DIR}/src/common +- ${HOSTAPD_DIR}/src/drivers +- ) ++ if(BWL_BUNDLED_WPA_CTRL) ++ ++ file(GLOB bwl_platform_sources ++ ${MODULE_PATH}/shared/*.c* ++ ${MODULE_PATH}/nl80211/*.c* ++ ++ # WPA Control Interface (bundled minimal client) ++ ${MODULE_PATH}/nl80211/wpa_ctrl_compat/wpa_ctrl_compat.c ++ ) ++ ++ include_directories( ++ ${MODULE_PATH}/nl80211/wpa_ctrl_compat ++ ) ++ ++ else() ++ ++ file(GLOB HOSTAPD_SEARCH_PATHS "${PLATFORM_BUILD_DIR}/hostapd*/hostapd-*") ++ find_path(HOSTAPD_INCLUDE_DIR NAMES "src/common/wpa_ctrl.h" PATHS ${HOSTAPD_SEARCH_PATHS} NO_CMAKE_FIND_ROOT_PATH) ++ set(HOSTAPD_DIR "${HOSTAPD_INCLUDE_DIR}") ++ ++ file(GLOB bwl_platform_sources ++ ${MODULE_PATH}/shared/*.c* ++ ${MODULE_PATH}/nl80211/*.c* ++ ++ # WPA Control Interface ++ ${HOSTAPD_DIR}/src/common/wpa_ctrl.c ++ ${HOSTAPD_DIR}/src/utils/os_unix.c ++ ) ++ ++ # Enable WPA control interface ++ add_definitions( ++ -DCONFIG_CTRL_IFACE ++ -DCONFIG_CTRL_IFACE_UNIX ++ ) ++ ++ # Hostapd/NL80211 include directories ++ include_directories( ++ ${HOSTAPD_DIR}/src/utils ++ ${HOSTAPD_DIR}/src/common ++ ${HOSTAPD_DIR}/src/drivers ++ ) ++ ++ endif() + + # Platform libraries + link_directories( +--- /dev/null ++++ b/common/beerocks/bwl/nl80211/wpa_ctrl_compat/wpa_ctrl.h +@@ -0,0 +1,68 @@ ++/* SPDX-License-Identifier: BSD-2-Clause-Patent ++ * ++ * SPDX-FileCopyrightText: 2026 the prplMesh contributors (see AUTHORS.md) ++ * ++ * This code is subject to the terms of the BSD+Patent license. ++ * See LICENSE file for more details. ++ */ ++ ++/* ++ * Minimal client for the hostapd / wpa_supplicant control interface. ++ * ++ * Self-contained implementation of the documented control interface ++ * protocol (https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html) with ++ * the same API as hostap's wpa_ctrl.h, so BWL can talk to the control ++ * sockets of an already installed hostapd without needing a hostapd ++ * source tree at build time. Only UNIX datagram sockets are supported. ++ */ ++ ++#ifndef BWL_WPA_CTRL_COMPAT_H_ ++#define BWL_WPA_CTRL_COMPAT_H_ ++ ++#include ++ ++#ifdef __cplusplus ++extern "C" { ++#endif ++ ++struct wpa_ctrl; ++ ++/* Open a connection to the daemon control socket at @ctrl_path. ++ * Returns NULL on failure. */ ++struct wpa_ctrl *wpa_ctrl_open(const char *ctrl_path); ++ ++/* Same as wpa_ctrl_open(), but create the local client socket inside ++ * @cli_path instead of /tmp. */ ++struct wpa_ctrl *wpa_ctrl_open2(const char *ctrl_path, const char *cli_path); ++ ++/* Close the connection and remove the local client socket. */ ++void wpa_ctrl_close(struct wpa_ctrl *ctrl); ++ ++/* Send @cmd and wait for the solicited reply. Unsolicited event messages ++ * arriving while waiting are passed to @msg_cb when given and dropped ++ * otherwise. On entry *reply_len holds the size of @reply; on success it ++ * is set to the reply size (the reply is not NUL terminated). Returns 0 ++ * on success, -1 on error and -2 on timeout. */ ++int wpa_ctrl_request(struct wpa_ctrl *ctrl, const char *cmd, size_t cmd_len, char *reply, ++ size_t *reply_len, void (*msg_cb)(char *msg, size_t len)); ++ ++/* Register (unregister) this connection as an event monitor. ++ * Returns 0 on success, -1 on failure and -2 on timeout. */ ++int wpa_ctrl_attach(struct wpa_ctrl *ctrl); ++int wpa_ctrl_detach(struct wpa_ctrl *ctrl); ++ ++/* Receive one pending message. Sizes behave as in wpa_ctrl_request(). ++ * Returns 0 on success and -1 on error. */ ++int wpa_ctrl_recv(struct wpa_ctrl *ctrl, char *reply, size_t *reply_len); ++ ++/* Return 1 when a message is waiting, 0 when not, -1 on error. */ ++int wpa_ctrl_pending(struct wpa_ctrl *ctrl); ++ ++/* File descriptor of the underlying socket for select()/poll() loops. */ ++int wpa_ctrl_get_fd(struct wpa_ctrl *ctrl); ++ ++#ifdef __cplusplus ++} ++#endif ++ ++#endif /* BWL_WPA_CTRL_COMPAT_H_ */ +--- /dev/null ++++ b/common/beerocks/bwl/nl80211/wpa_ctrl_compat/wpa_ctrl_compat.c +@@ -0,0 +1,267 @@ ++/* SPDX-License-Identifier: BSD-2-Clause-Patent ++ * ++ * SPDX-FileCopyrightText: 2026 the prplMesh contributors (see AUTHORS.md) ++ * ++ * This code is subject to the terms of the BSD+Patent license. ++ * See LICENSE file for more details. ++ */ ++ ++#include "wpa_ctrl.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++/* Directory for the local (client) end of the connection. The daemon only ++ * replies to a bound datagram socket, so every connection creates one. */ ++#define WPA_CTRL_CLIENT_DIR "/tmp" ++ ++/* How long wpa_ctrl_request() waits for a solicited reply. Same value as ++ * the reference client so BWL retry counting keeps its meaning. */ ++#define WPA_CTRL_REPLY_TIMEOUT_SEC 10 ++ ++/* OpenWrt runs hostapd inside a procd jail as user/group "network" while ++ * BWL usually runs as root. The daemon must be able to send datagrams to ++ * our client socket, so the socket node is made group accessible and ++ * handed over to that account (statically allocated as 101 on OpenWrt, ++ * resolved by name when possible). */ ++#define WPA_CTRL_DAEMON_FALLBACK_ID 101 ++ ++struct wpa_ctrl { ++ int s; ++ struct sockaddr_un local; ++ struct sockaddr_un dest; ++}; ++ ++static void wpa_ctrl_grant_daemon_access(const char *path) ++{ ++ struct group *grp = getgrnam("network"); ++ struct passwd *pwd = getpwnam("network"); ++ gid_t gid = grp ? grp->gr_gid : (gid_t)WPA_CTRL_DAEMON_FALLBACK_ID; ++ uid_t uid = pwd ? pwd->pw_uid : (uid_t)WPA_CTRL_DAEMON_FALLBACK_ID; ++ ++ /* Group first so restricted processes still hand over what they can; ++ * when even that fails the socket keeps working for a root daemon. */ ++ if (lchown(path, (uid_t)-1, gid) != 0 || lchown(path, uid, gid) != 0) { ++ /* best effort by design */ ++ } ++} ++ ++struct wpa_ctrl *wpa_ctrl_open2(const char *ctrl_path, const char *cli_path) ++{ ++ struct wpa_ctrl *ctrl; ++ static unsigned int counter = 0; ++ unsigned int tries = 0; ++ size_t dest_len; ++ int flags; ++ int written; ++ ++ if (!ctrl_path) { ++ return NULL; ++ } ++ if (!cli_path) { ++ cli_path = WPA_CTRL_CLIENT_DIR; ++ } ++ ++ ctrl = calloc(1, sizeof(*ctrl)); ++ if (!ctrl) { ++ return NULL; ++ } ++ ++ ctrl->s = socket(PF_UNIX, SOCK_DGRAM, 0); ++ if (ctrl->s < 0) { ++ free(ctrl); ++ return NULL; ++ } ++ ++ /* Permissions are inherited by the socket node bind() creates, so set ++ * them up front and there is no window without daemon access. */ ++ (void)fchmod(ctrl->s, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); ++ ++ ctrl->local.sun_family = AF_UNIX; ++ ++try_again: ++ written = snprintf(ctrl->local.sun_path, sizeof(ctrl->local.sun_path), "%s/wpa_ctrl_%d-%u", ++ cli_path, (int)getpid(), counter); ++ if (written < 0 || (size_t)written >= sizeof(ctrl->local.sun_path)) { ++ close(ctrl->s); ++ free(ctrl); ++ return NULL; ++ } ++ tries++; ++ if (bind(ctrl->s, (struct sockaddr *)&ctrl->local, sizeof(ctrl->local)) < 0) { ++ if (errno == EADDRINUSE && tries < 2) { ++ /* A previous process that had our PID left the node behind; ++ * reclaim the name. */ ++ unlink(ctrl->local.sun_path); ++ goto try_again; ++ } ++ if (tries < 5) { ++ counter++; ++ goto try_again; ++ } ++ close(ctrl->s); ++ free(ctrl); ++ return NULL; ++ } ++ counter++; ++ ++ wpa_ctrl_grant_daemon_access(ctrl->local.sun_path); ++ ++ dest_len = strlen(ctrl_path); ++ if (dest_len >= sizeof(ctrl->dest.sun_path)) { ++ goto fail; ++ } ++ ctrl->dest.sun_family = AF_UNIX; ++ memcpy(ctrl->dest.sun_path, ctrl_path, dest_len + 1); ++ if (connect(ctrl->s, (struct sockaddr *)&ctrl->dest, sizeof(ctrl->dest)) < 0) { ++ goto fail; ++ } ++ ++ /* The connection is never meant for processes BWL spawns. */ ++ flags = fcntl(ctrl->s, F_GETFD); ++ if (flags >= 0) { ++ (void)fcntl(ctrl->s, F_SETFD, flags | FD_CLOEXEC); ++ } ++ ++ return ctrl; ++ ++fail: ++ unlink(ctrl->local.sun_path); ++ close(ctrl->s); ++ free(ctrl); ++ return NULL; ++} ++ ++struct wpa_ctrl *wpa_ctrl_open(const char *ctrl_path) ++{ ++ return wpa_ctrl_open2(ctrl_path, NULL); ++} ++ ++void wpa_ctrl_close(struct wpa_ctrl *ctrl) ++{ ++ if (!ctrl) { ++ return; ++ } ++ unlink(ctrl->local.sun_path); ++ close(ctrl->s); ++ free(ctrl); ++} ++ ++int wpa_ctrl_request(struct wpa_ctrl *ctrl, const char *cmd, size_t cmd_len, char *reply, ++ size_t *reply_len, void (*msg_cb)(char *msg, size_t len)) ++{ ++ struct timeval tv; ++ fd_set rfds; ++ ssize_t res; ++ int sres; ++ ++ if (!ctrl || !cmd || !reply || !reply_len || !*reply_len) { ++ return -1; ++ } ++ ++ if (send(ctrl->s, cmd, cmd_len, 0) < 0) { ++ return -1; ++ } ++ ++ for (;;) { ++ tv.tv_sec = WPA_CTRL_REPLY_TIMEOUT_SEC; ++ tv.tv_usec = 0; ++ FD_ZERO(&rfds); ++ FD_SET(ctrl->s, &rfds); ++ sres = select(ctrl->s + 1, &rfds, NULL, NULL, &tv); ++ if (sres < 0) { ++ if (errno == EINTR) { ++ continue; ++ } ++ return -1; ++ } ++ if (!FD_ISSET(ctrl->s, &rfds)) { ++ return -2; ++ } ++ ++ res = recv(ctrl->s, reply, *reply_len, 0); ++ if (res < 0) { ++ if (errno == EINTR) { ++ continue; ++ } ++ return -1; ++ } ++ if (res > 0 && ++ (reply[0] == '<' || ((size_t)res > 7 && strncmp(reply, "IFNAME=", 7) == 0))) { ++ /* Unsolicited event, not the reply we are waiting for. */ ++ if (msg_cb) { ++ size_t event_len = (size_t)res; ++ if (event_len == *reply_len) { ++ event_len--; ++ } ++ reply[event_len] = '\0'; ++ msg_cb(reply, event_len); ++ } ++ continue; ++ } ++ *reply_len = (size_t)res; ++ return 0; ++ } ++} ++ ++static int wpa_ctrl_attach_helper(struct wpa_ctrl *ctrl, int attach) ++{ ++ char reply[10]; ++ size_t reply_len = sizeof(reply); ++ int ret; ++ ++ ret = wpa_ctrl_request(ctrl, attach ? "ATTACH" : "DETACH", 6, reply, &reply_len, NULL); ++ if (ret) { ++ return ret; ++ } ++ return (reply_len == 3 && memcmp(reply, "OK\n", 3) == 0) ? 0 : -1; ++} ++ ++int wpa_ctrl_attach(struct wpa_ctrl *ctrl) { return wpa_ctrl_attach_helper(ctrl, 1); } ++ ++int wpa_ctrl_detach(struct wpa_ctrl *ctrl) { return wpa_ctrl_attach_helper(ctrl, 0); } ++ ++int wpa_ctrl_recv(struct wpa_ctrl *ctrl, char *reply, size_t *reply_len) ++{ ++ ssize_t res; ++ ++ if (!ctrl || !reply || !reply_len) { ++ return -1; ++ } ++ res = recv(ctrl->s, reply, *reply_len, 0); ++ if (res < 0) { ++ return -1; ++ } ++ *reply_len = (size_t)res; ++ return 0; ++} ++ ++int wpa_ctrl_pending(struct wpa_ctrl *ctrl) ++{ ++ struct timeval tv = {0, 0}; ++ fd_set rfds; ++ ++ if (!ctrl) { ++ return -1; ++ } ++ FD_ZERO(&rfds); ++ FD_SET(ctrl->s, &rfds); ++ if (select(ctrl->s + 1, &rfds, NULL, NULL, &tv) < 0) { ++ return -1; ++ } ++ return FD_ISSET(ctrl->s, &rfds) ? 1 : 0; ++} ++ ++int wpa_ctrl_get_fd(struct wpa_ctrl *ctrl) { return ctrl ? ctrl->s : -1; }