mirror of https://github.com/openwrt/packages.git
prplmesh: add package
Add the prplMesh 6.0.1 EasyMesh controller and agent for stock OpenWrt. The package keeps the installed hostapd/wpad stack and drives it over the existing UCI, ubus, nl80211 and hostapd control sockets. The hostapd control interface client is a small self-contained implementation added by patch 120, so no hostapd source tree is downloaded, bundled or duplicated at build time and the system hostapd stays the only hostapd. Binaries install under /usr/libexec/prplmesh, read-only data under /usr/share/prplmesh, shared libraries under /usr/lib, configuration under /etc/config/prplmesh. Of the prplOS helper scripts only prplmesh_utils.sh is installed: patch 161 teaches its platform guard stock OpenWrt and the init's daily log roller invokes it; the other helpers stay uninstalled. Its option parsing needs util-linux getopt (the busybox applet is off by default), hence the getopt dependency. At build time prplMesh generates its IEEE 1905 / EasyMesh TLV classes from YAML definitions with its own Python generator, hence the host Python and PyYAML build dependencies. Stock builds run without NBAPI, so the controller skips the CurrentOperatingClassProfile data-model cleanup for radios without a data model path, and legacy vendor diagnostic measurements default to off while standard EasyMesh metrics stay enabled. The controller also registers radios that appear only in Topology Responses: some stock agents never run WSC onboarding for a band (Wi-Fi 7 units and their 6 GHz radio), and such radios, their BSSes and their clients stayed invisible otherwise. Agents in turn import clients that are already associated when the agent starts: patch 150 walks hostapd's STA-FIRST/STA-NEXT list on the nl80211 AP and monitor HALs and reconciles the result every 60 seconds, so stations that associated while the agent was down appear without waiting for a reassociation, and stale entries are disconnected only after a cleanly enumerated sweep. The init preflights every binary the selected management mode needs before registering any procd instance, and the example credentials ship empty so an enabled but unconfigured service fails validation loudly instead of deploying a published passphrase. A test-version.sh verifies the installed version from the shipped version file instead of probing daemons, and a test.sh checks the shipped log levels and the roll-helper install. Production log levels default to error, info, warning and fatal for file and syslog output of both daemons. Log retention is enforced by the upstream roll helper (its platform guard now accepts openwrt): the init prunes previous-boot logs once per boot before the daemons come up (a /var/run stamp keeps procd reload triggers, which re-enter start_service with the daemons live, from re-rolling their logs) and a procd-supervised roller repeats the roll daily; an image stripped of the helper runs without rotation instead of refusing to start. Link-metrics reporting no longer warns about wireless-capable radios deliberately not used for backhaul. An earlier revision of this integration, differing only in the wpa_ctrl client origin and the install prefix, runs on OpenWrt 25.12.5 mediatek/mt7622 with two Ethernet-connected EasyMesh agents. Ref: https://gitlab.com/prpl-foundation/prplmesh/prplMesh Ref: https://gitlab.com/kreout/prpl-mesh-mercusys/-/merge_requests/1 Signed-off-by: Andrei Ovcharenko <a@krot.name>pull/30532/head
parent
462e6ceda8
commit
1bb94b900f
|
|
@ -0,0 +1,76 @@
|
|||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=prplmesh
|
||||
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 <a@krot.name>
|
||||
|
||||
# The tlvf code generator (framework/tlvf/tlvf.py) runs at build time and
|
||||
# needs a host Python with PyYAML
|
||||
PKG_BUILD_DEPENDS:=python3/host 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
|
||||
|
||||
define Package/prplmesh
|
||||
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 +getopt
|
||||
endef
|
||||
|
||||
define Package/prplmesh/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/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/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/prplmesh/bin $(1)/usr/libexec/prplmesh/scripts $(1)/usr/share/prplmesh/config $(1)/usr/lib $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/bin/* $(1)/usr/libexec/prplmesh/bin/
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/scripts/prplmesh_utils.sh $(1)/usr/libexec/prplmesh/scripts/
|
||||
$(INSTALL_DATA) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/share/* $(1)/usr/share/prplmesh/
|
||||
$(INSTALL_DATA) $(PKG_INSTALL_DIR)/usr/libexec/prplmesh/config/* $(1)/usr/share/prplmesh/config/
|
||||
$(LN) ../../share/prplmesh $(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))
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# beerocks derives radio and VAP identifiers from wlan<N> interface names,
|
||||
# so the fronthaul BSS netdevs must be named wlan<N> (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 ''
|
||||
option mode_enabled 'WPA2-Personal'
|
||||
option key_passphrase ''
|
||||
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 diagnostics_measurements '0'
|
||||
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 ''
|
||||
option key ''
|
||||
option encryption 'sae-mixed'
|
||||
option bands '2g 5g'
|
||||
option fronthaul '1'
|
||||
option backhaul '0'
|
||||
|
||||
config bss_profile 'main_6'
|
||||
option ssid ''
|
||||
option key ''
|
||||
option encryption 'sae'
|
||||
option bands '6g'
|
||||
option fronthaul '1'
|
||||
option backhaul '0'
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
#!/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
|
||||
LOG_ROLL_INTERVAL_SECONDS=86400
|
||||
|
||||
mesh_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
|
||||
}
|
||||
|
||||
register_log_roller() {
|
||||
local helper="$PRPLMESH_DIR/scripts/prplmesh_utils.sh"
|
||||
|
||||
procd_open_instance log-roller
|
||||
# shellcheck disable=SC2016
|
||||
procd_set_param command /bin/sh -c '
|
||||
helper=$1
|
||||
interval=$2
|
||||
while sleep "$interval"; do
|
||||
"$helper" roll_logs || logger -t prplmesh "log roll failed"
|
||||
done' sh "$helper" "$LOG_ROLL_INTERVAL_SECONDS"
|
||||
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 log_roller=1
|
||||
|
||||
mesh_enabled || return 0
|
||||
|
||||
# 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
|
||||
|
||||
[ -x "$PRPLMESH_DIR/bin/ieee1905_transport" ] || {
|
||||
logger -t prplmesh "ieee1905_transport is missing or not executable"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Log rotation is not worth keeping the mesh down for: run without
|
||||
# it when the helper is stripped from the image.
|
||||
[ -x "$PRPLMESH_DIR/scripts/prplmesh_utils.sh" ] || {
|
||||
logger -t prplmesh "log-roll helper is missing or not executable; log rotation disabled"
|
||||
log_roller=0
|
||||
}
|
||||
|
||||
if [ "$need_controller" = 1 ]; then
|
||||
[ -x "$PRPLMESH_DIR/bin/beerocks_controller" ] || {
|
||||
logger -t prplmesh "beerocks_controller is missing or not executable"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ "$need_agent" = 1 ]; then
|
||||
[ -x "$PRPLMESH_DIR/bin/beerocks_agent" ] || {
|
||||
logger -t prplmesh "beerocks_agent is missing or not executable"
|
||||
return 1
|
||||
}
|
||||
[ -n "$(uci -q get prplmesh.config.mandatory_interfaces)" ] || {
|
||||
logger -t prplmesh "mandatory_interfaces not set; agent left stopped"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ "$log_roller" = 1 ] && [ ! -e /var/run/prplmesh.boot_roll ]; then
|
||||
# Prune previous-boot logs before the daemons open new ones: the
|
||||
# helper only deletes old rotations and signals running daemons,
|
||||
# and none of ours are up yet, so nothing is rotated out early.
|
||||
# Reload triggers re-enter start_service with the daemons up
|
||||
# (procd reload falls through to start), so stamp the roll as
|
||||
# done for this boot; /var/run is tmpfs and clears the stamp.
|
||||
"$PRPLMESH_DIR/scripts/prplmesh_utils.sh" roll_logs >/dev/null 2>&1 \
|
||||
|| logger -t prplmesh "startup log roll failed"
|
||||
touch /var/run/prplmesh.boot_roll
|
||||
fi
|
||||
|
||||
register_process transport "$PRPLMESH_DIR/bin/ieee1905_transport"
|
||||
|
||||
if [ "$log_roller" = 1 ]; then
|
||||
register_log_roller
|
||||
fi
|
||||
|
||||
if [ "$need_controller" = 1 ]; then
|
||||
register_process controller "$PRPLMESH_DIR/bin/beerocks_controller"
|
||||
fi
|
||||
|
||||
if [ "$need_agent" = 1 ]; then
|
||||
# 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
|
||||
}
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
From 3e5e89033a8118021d176c86122f07229110e46f Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Tue, 21 Jul 2026 22:33:14 +0300
|
||||
Subject: [PATCH] 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.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
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 <bcl/beerocks_event_loop.h>
|
||||
#include <bcl/beerocks_ucc_server.h>
|
||||
|
||||
+#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
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 <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace beerocks {
|
||||
--- a/framework/platform/bpl/cfg/uci/bpl.cpp
|
||||
+++ b/framework/platform/bpl/cfg/uci/bpl.cpp
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <bpl/bpl.h>
|
||||
+#include <bpl/bpl_amx.h>
|
||||
|
||||
#include <mapf/common/logger.h>
|
||||
|
||||
--- 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 <bcl/beerocks_string_utils.h>
|
||||
+#include <bcl/beerocks_version.h>
|
||||
#include <mapf/common/logger.h>
|
||||
#include <mapf/common/utils.h>
|
||||
|
||||
+#include <fstream>
|
||||
+
|
||||
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 <mapf/common/logger.h>
|
||||
#include <mapf/common/utils.h>
|
||||
@@ -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<std::string> sections;
|
||||
+ if (!uci_get_all_sections("wireless", "wifi-iface", sections)) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ std::vector<std::string> 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<std::string, std::string> &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
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "bpl_cfg_uci.h"
|
||||
#include "../../common/utils/utils.h"
|
||||
+#include "bpl_cfg_helper.h"
|
||||
#include <easylogging++.h>
|
||||
#include <mapf/common/utils.h>
|
||||
|
||||
@@ -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.
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
From f5ba6681fa3f980eeb5109726bf00d34c50c560c Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
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 <a@krot.name>
|
||||
---
|
||||
.../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 <bcl/beerocks_string_utils.h>
|
||||
#include <mapf/common/logger.h>
|
||||
#include <mapf/common/utils.h>
|
||||
|
||||
+#include <sstream>
|
||||
+#include <utility>
|
||||
+
|
||||
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<son::wireless_utils::sBssInfoConf> &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<son::wireless_utils::sBssInfoConf> &wireless_settings)
|
||||
+{
|
||||
+ std::vector<std::string> 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<son::wireless_utils::sBssInfoConf> &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 <algorithm>
|
||||
+#include <cctype>
|
||||
+#include <chrono>
|
||||
+#include <list>
|
||||
+#include <string>
|
||||
+#include <unordered_set>
|
||||
+#include <vector>
|
||||
+
|
||||
+#include <bcl/son/son_wireless_utils.h>
|
||||
+
|
||||
+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<uint16_t>(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<uint8_t> &operating_classes)
|
||||
+{
|
||||
+ std::string value = band_or_hwmode;
|
||||
+ std::transform(value.begin(), value.end(), value.begin(),
|
||||
+ [](unsigned char c) { return static_cast<char>(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<char>(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<std::string> &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
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../../common/include>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
|
||||
)
|
||||
+
|
||||
+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
|
||||
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../../common/beerocks/bcl/include>
|
||||
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../cfg/uci>
|
||||
+)
|
||||
+add_test(NAME bpl_cfg_wifi_utils_test COMMAND $<TARGET_FILE:bpl_cfg_wifi_utils_test>)
|
||||
--- /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 <gtest/gtest.h>
|
||||
+
|
||||
+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<uint16_t>(transition) & WSC::eWscAuth::WSC_AUTH_WPA2PSK);
|
||||
+ EXPECT_NE(0, static_cast<uint16_t>(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::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<uint8_t> 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<std::string> none;
|
||||
+ const std::vector<std::string> one{"default_radio0"};
|
||||
+ const std::vector<std::string> 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 <asm/byteorder.h>
|
||||
#include <cstddef>
|
||||
+#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace assoc_frame {
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
From 918ec01a76995deca767c5bc98e32de3573b1385 Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
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.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
common/beerocks/bwl/CMakeLists.txt | 64 +++--
|
||||
.../bwl/nl80211/wpa_ctrl_compat/wpa_ctrl.h | 68 +++++
|
||||
.../nl80211/wpa_ctrl_compat/wpa_ctrl_compat.c | 272 ++++++++++++++++++
|
||||
3 files changed, 383 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 <stddef.h>
|
||||
+
|
||||
+#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,272 @@
|
||||
+/* 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 <errno.h>
|
||||
+#include <fcntl.h>
|
||||
+#include <grp.h>
|
||||
+#include <pwd.h>
|
||||
+#include <stdio.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <string.h>
|
||||
+#include <sys/select.h>
|
||||
+#include <sys/socket.h>
|
||||
+#include <sys/stat.h>
|
||||
+#include <sys/time.h>
|
||||
+#include <sys/types.h>
|
||||
+#include <sys/un.h>
|
||||
+#include <unistd.h>
|
||||
+
|
||||
+/* 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,
|
||||
+ * then full ownership; each step is best effort by design - even when
|
||||
+ * both fail the socket keeps working for a root daemon. */
|
||||
+ if (lchown(path, (uid_t)-1, gid) != 0) {
|
||||
+ /* best effort by design */
|
||||
+ }
|
||||
+ if (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;
|
||||
+ }
|
||||
+
|
||||
+ /* Set client socket file permissions so that bind() creates the client
|
||||
+ * socket with these permissions and there is no need to try to change
|
||||
+ * them with chmod() after bind() which would have potential issues with
|
||||
+ * race conditions. These permissions are needed to make sure the jailed
|
||||
+ * hostapd on OpenWrt can send replies to this socket. */
|
||||
+ if (fchmod(ctrl->s, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) != 0) {
|
||||
+ /* best effort by design */
|
||||
+ }
|
||||
+
|
||||
+ 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;
|
||||
+
|
||||
+ 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);
|
||||
+ int 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; }
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
From 178d8d572457c4347f04fe106570781e3901fe0c Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Tue, 28 Jul 2026 22:45:00 +0300
|
||||
Subject: [PATCH] controller: skip op class DM cleanup without a data model
|
||||
|
||||
Stock OpenWrt builds prplMesh without NBAPI, leaving radio data model
|
||||
paths empty. The operating channel report handler already treats that
|
||||
state as valid, but the follow-up cleanup still tries to update five
|
||||
empty CurrentOperatingClassProfile paths per radio. This floods the
|
||||
controller log even though the operational channel is updated.
|
||||
|
||||
Mirror the existing guard in handle_current_op_class and skip the
|
||||
data-model-only cleanup when the radio has no data model path.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
controller/src/beerocks/master/db/db.cpp | 4 ++++
|
||||
1 file changed, 4 insertions(+)
|
||||
|
||||
--- a/controller/src/beerocks/master/db/db.cpp
|
||||
+++ b/controller/src/beerocks/master/db/db.cpp
|
||||
@@ -7087,6 +7087,10 @@ bool db::dm_clear_empty_current_op_class
|
||||
return false;
|
||||
}
|
||||
|
||||
+ if (radio->dm_path.empty()) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
for (int i = 0; i < Agent::operatingClassProfileIndex::OPERATING_CLASS_MAX_BW; i++) {
|
||||
if (radio->current_operating_class_profile[i].op_class == 0) {
|
||||
set_dm_current_op_class(radio_mac, i, 0, 0, 0);
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
From 678ee6b029f3e7a5e90363cb58e79412d899fdac Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Wed, 29 Jul 2026 09:40:29 +0300
|
||||
Subject: [PATCH] controller: register foreign radios from Topology Response
|
||||
|
||||
Some stock agents run WSC onboarding only for a subset of their radios:
|
||||
Mercusys Wi-Fi 7 units, for example, never send an AP-Autoconfiguration
|
||||
Search (and therefore no WSC M1) for their 6 GHz radio, while still
|
||||
reporting it in the AP Operational BSS TLV, Operating Channel Reports,
|
||||
AP Metrics Responses and Channel Selection Responses. The controller
|
||||
database only creates radio nodes in the WSC M1 flow, so every report
|
||||
about such a radio was dropped and the radio, its BSSes and its clients
|
||||
never appeared in the topology.
|
||||
|
||||
Register unknown radios from the AP Operational BSS TLV of a Topology
|
||||
Response instead of discarding them, and let handle_current_op_class
|
||||
learn the radio band from the first reported operating class for radios
|
||||
created this way (they start as FREQ_UNKNOWN).
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
controller/src/beerocks/master/db/db.cpp | 12 +++++++++--
|
||||
.../beerocks/master/tasks/topology_task.cpp | 21 +++++++++++++++++--
|
||||
2 files changed, 29 insertions(+), 4 deletions(-)
|
||||
|
||||
--- a/controller/src/beerocks/master/db/db.cpp
|
||||
+++ b/controller/src/beerocks/master/db/db.cpp
|
||||
@@ -6998,9 +6998,17 @@ bool db::handle_current_op_class(const s
|
||||
return false;
|
||||
}
|
||||
|
||||
- if (radio->band != son::wireless_utils::which_freq_op_cls(op_class)) {
|
||||
+ auto op_class_band = son::wireless_utils::which_freq_op_cls(op_class);
|
||||
+ if (radio->band == beerocks::FREQ_UNKNOWN) {
|
||||
+ // Radios registered from a Topology Response (agents that skip WSC
|
||||
+ // onboarding for a band) start without a band; learn it from the
|
||||
+ // first reported operating class.
|
||||
+ LOG(INFO) << "Radio " << radio_mac << " band learned from op class " << int(op_class)
|
||||
+ << ": " << op_class_band;
|
||||
+ radio->band = op_class_band;
|
||||
+ } else if (radio->band != op_class_band) {
|
||||
LOG(ERROR) << "This should not happen. Radio band changed from " << radio->band << " to "
|
||||
- << son::wireless_utils::which_freq_op_cls(op_class);
|
||||
+ << op_class_band;
|
||||
return false;
|
||||
}
|
||||
|
||||
--- a/controller/src/beerocks/master/tasks/topology_task.cpp
|
||||
+++ b/controller/src/beerocks/master/tasks/topology_task.cpp
|
||||
@@ -303,9 +303,26 @@ bool topology_task::handle_topology_resp
|
||||
// Update BSSes in the Agent
|
||||
auto radio = database.get_radio(src_mac, radio_entry.radio_uid());
|
||||
if (!radio) {
|
||||
+ // Some stock agents never run WSC onboarding for one of their
|
||||
+ // radios (e.g. 6 GHz on Wi-Fi 7 units), so the radio is absent
|
||||
+ // from the database and every report about it would be dropped
|
||||
+ // here. Register it from the Topology Response instead; the
|
||||
+ // band and channel are learned from the next Operating Channel
|
||||
+ // Report.
|
||||
LOG(WARNING) << "OperationalBSS on unknown radio " << radio_entry.radio_uid()
|
||||
- << " on " << src_mac;
|
||||
- continue;
|
||||
+ << " on " << src_mac << ", registering it";
|
||||
+ if (!database.add_radio(radio_entry.radio_uid(), src_mac)) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ radio = database.get_radio(src_mac, radio_entry.radio_uid());
|
||||
+ if (!radio) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ // The radio carries operational BSSes, so mark it active for
|
||||
+ // the connection map right away; the WSC onboarding flow will
|
||||
+ // never do it for radios it does not see.
|
||||
+ son_actions::set_radio_active(database, tasks,
|
||||
+ tlvf::mac_to_string(radio_entry.radio_uid()), true);
|
||||
}
|
||||
|
||||
radio->bsses.keep_new_prepare();
|
||||
|
|
@ -0,0 +1,735 @@
|
|||
From 50be87526568d93de752bdd220f6ef0095623784 Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Wed, 29 Jul 2026 13:20:19 +0300
|
||||
Subject: [PATCH] bwl: import and reconcile connected clients on nl80211
|
||||
|
||||
The nl80211 flavor left pre/generate_connected_clients_events() as
|
||||
stubs that report success, so stations already associated when the
|
||||
agent starts never reach the controller: BML shows only clients that
|
||||
(re)associate afterwards, keeps stale parents and reports RSSI -127.
|
||||
|
||||
Port the DWPAL station sweep to the nl80211 AP and monitor HALs: walk
|
||||
every VAP with STA-FIRST/STA-NEXT over the hostapd control socket,
|
||||
respect the iteration time budget, validate and deduplicate stations,
|
||||
skip authenticated-but-not-associated entries, survive a station
|
||||
vanishing mid-iteration (bounded VAP restart), and stop a failed VAP
|
||||
without wedging the FSM. Station capabilities are parsed from the
|
||||
hostapd station dump.
|
||||
|
||||
The AP HAL keeps the sweep idempotent: it tracks currently connected
|
||||
stations from runtime events, emits an association only for new or
|
||||
moved stations, and reconciles stale clients only for VAPs whose
|
||||
enumeration reached a clean end-of-list (an empty reply; FAIL past the
|
||||
restart budget leaves the VAP partially walked). A pre-sweep snapshot,
|
||||
invalidated by runtime events, prevents a concurrent association from
|
||||
being undone. The new supports_connected_clients_reconciliation()
|
||||
capability lets the AP manager re-run the sweep every 60 seconds; HALs
|
||||
without that guarantee keep the previous run-once behavior.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
.../ap_manager/ap_manager.cpp | 14 +
|
||||
.../fronthaul_manager/ap_manager/ap_manager.h | 2 +
|
||||
common/beerocks/bwl/include/bwl/ap_wlan_hal.h | 8 +
|
||||
.../bwl/nl80211/ap_wlan_hal_nl80211.cpp | 337 +++++++++++++++++-
|
||||
.../bwl/nl80211/ap_wlan_hal_nl80211.h | 17 +
|
||||
.../bwl/nl80211/base_wlan_hal_nl80211.cpp | 19 +
|
||||
.../bwl/nl80211/base_wlan_hal_nl80211.h | 3 +
|
||||
.../bwl/nl80211/mon_wlan_hal_nl80211.cpp | 129 ++++++-
|
||||
.../bwl/nl80211/mon_wlan_hal_nl80211.h | 12 +
|
||||
9 files changed, 526 insertions(+), 15 deletions(-)
|
||||
|
||||
--- a/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.cpp
|
||||
+++ b/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.cpp
|
||||
@@ -747,6 +747,20 @@ bool ApManager::ap_manager_fsm(bool &con
|
||||
m_generate_connected_clients_events = !is_finished_all_clients;
|
||||
}
|
||||
|
||||
+ // Periodic reconciliation: HALs with an idempotent sweep re-run it to
|
||||
+ // recover association events lost while the control socket was busy
|
||||
+ // and to clean up stale clients.
|
||||
+ if (!m_generate_connected_clients_events &&
|
||||
+ ap_wlan_hal->supports_connected_clients_reconciliation() &&
|
||||
+ now > m_next_client_reconciliation_time) {
|
||||
+ m_next_client_reconciliation_time = now + std::chrono::seconds(60);
|
||||
+ if (ap_wlan_hal->pre_generate_connected_clients_events()) {
|
||||
+ m_generate_connected_clients_events = true;
|
||||
+ } else {
|
||||
+ LOG(WARNING) << "Failed to prepare connected clients reconciliation";
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
// Allow clients with expired blocking period timer
|
||||
allow_expired_clients();
|
||||
break;
|
||||
--- a/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.h
|
||||
+++ b/agent/src/beerocks/fronthaul_manager/ap_manager/ap_manager.h
|
||||
@@ -300,6 +300,8 @@ private:
|
||||
bool m_generate_connected_clients_events = false;
|
||||
std::chrono::steady_clock::time_point m_next_generate_connected_events_time =
|
||||
std::chrono::steady_clock::time_point::min();
|
||||
+ std::chrono::steady_clock::time_point m_next_client_reconciliation_time =
|
||||
+ std::chrono::steady_clock::now() + std::chrono::seconds(60);
|
||||
|
||||
//Timer for triggering a CSA notification
|
||||
void start_csa_notification_timer(
|
||||
--- a/common/beerocks/bwl/include/bwl/ap_wlan_hal.h
|
||||
+++ b/common/beerocks/bwl/include/bwl/ap_wlan_hal.h
|
||||
@@ -442,6 +442,14 @@ public:
|
||||
virtual bool pre_generate_connected_clients_events() = 0;
|
||||
|
||||
/**
|
||||
+ * @brief Whether generate_connected_clients_events() is idempotent for
|
||||
+ * this HAL (unchanged clients produce no duplicate events and stale
|
||||
+ * clients are reported as disconnected), so the AP manager may re-run
|
||||
+ * it periodically to reconcile missed association events.
|
||||
+ */
|
||||
+ virtual bool supports_connected_clients_reconciliation() const { return false; }
|
||||
+
|
||||
+ /**
|
||||
* @brief Start WPS PBC procedure on a given VAP
|
||||
*
|
||||
* @param iface_name VAP interface on which to start WPS PBC
|
||||
--- a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp
|
||||
+++ b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <netlink/genl/genl.h>
|
||||
#include <netlink/msg.h>
|
||||
#include <netlink/netlink.h>
|
||||
+#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -1291,18 +1292,344 @@ bool ap_wlan_hal_nl80211::set_disabled_s
|
||||
return true;
|
||||
}
|
||||
|
||||
+// IEEE 802.11 HT capability bits needed to interpret hostapd station dumps;
|
||||
+// the stock build has no hostapd headers to take them from.
|
||||
+static constexpr uint16_t NL80211_HT_CAP_SUPP_CHANNEL_WIDTH_SET = 0x0002;
|
||||
+static constexpr uint16_t NL80211_HT_CAP_SMPS_MASK = 0x000C;
|
||||
+static constexpr uint16_t NL80211_HT_CAP_SHORT_GI20MHZ = 0x0020;
|
||||
+static constexpr uint16_t NL80211_HT_CAP_SHORT_GI40MHZ = 0x0040;
|
||||
+
|
||||
+static void sta_caps_from_ht(const int *HT_MCS, const std::string &ht_cap_str,
|
||||
+ beerocks::message::sRadioCapabilities &sta_caps)
|
||||
+{
|
||||
+ sta_caps.ht_bw = beerocks::BANDWIDTH_UNKNOWN;
|
||||
+
|
||||
+ if (ht_cap_str.empty()) {
|
||||
+ sta_caps.ant_num = 1;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ uint16_t ht_cap = uint16_t(std::strtoul(ht_cap_str.c_str(), nullptr, 16));
|
||||
+ sta_caps.ht_bw = (ht_cap & NL80211_HT_CAP_SUPP_CHANNEL_WIDTH_SET) ? beerocks::BANDWIDTH_40
|
||||
+ : beerocks::BANDWIDTH_20;
|
||||
+ sta_caps.ht_sm_power_save = ((ht_cap & NL80211_HT_CAP_SMPS_MASK) >> 2) & 0x03;
|
||||
+ sta_caps.ht_low_bw_short_gi = (ht_cap & NL80211_HT_CAP_SHORT_GI20MHZ) != 0;
|
||||
+ sta_caps.ht_high_bw_short_gi = (ht_cap & NL80211_HT_CAP_SHORT_GI40MHZ) != 0;
|
||||
+
|
||||
+ uint32_t ht_mcs = 0;
|
||||
+ for (uint8_t i = 0; i < 4; i++) {
|
||||
+ ht_mcs |= uint32_t(HT_MCS[i]) << (8 * i);
|
||||
+ }
|
||||
+ uint32_t mask = 0x80000000;
|
||||
+ for (uint8_t i = 4; i > 0; i--) { // 4ss
|
||||
+ for (int8_t j = 7; j >= 0; j--) { // 8 bits
|
||||
+ if ((ht_mcs & mask) > 0) {
|
||||
+ sta_caps.ht_ss = i;
|
||||
+ sta_caps.ant_num = i;
|
||||
+ sta_caps.ht_mcs = j;
|
||||
+ return;
|
||||
+ }
|
||||
+ mask /= 2;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void sta_caps_from_vht(const int16_t *VHT_MCS, const std::string &vht_cap_str,
|
||||
+ beerocks::message::sRadioCapabilities &sta_caps)
|
||||
+{
|
||||
+ sta_caps.vht_bw = beerocks::BANDWIDTH_UNKNOWN;
|
||||
+
|
||||
+ if (!vht_cap_str.empty()) {
|
||||
+ uint32_t vht_cap = std::strtoul(vht_cap_str.c_str(), nullptr, 16);
|
||||
+ uint8_t supported_bw_bits = (vht_cap >> 2) & 0x03;
|
||||
+
|
||||
+ sta_caps.vht_bw =
|
||||
+ (supported_bw_bits == 0) ? beerocks::BANDWIDTH_80 : beerocks::BANDWIDTH_160;
|
||||
+ sta_caps.vht_low_bw_short_gi = (vht_cap >> 5) & 0x01;
|
||||
+ sta_caps.vht_high_bw_short_gi = (vht_cap >> 6) & 0x01;
|
||||
+
|
||||
+ uint16_t vht_mcs_rx = uint16_t(VHT_MCS[0]);
|
||||
+ for (uint8_t i = 4; i > 0; i--) { // 4ss
|
||||
+ uint16_t vht_mcs_temp = (vht_mcs_rx >> (2 * (i - 1))) & 0x03;
|
||||
+ if (vht_mcs_temp != 0x3) { // 0x3 == not supported
|
||||
+ sta_caps.vht_ss = i;
|
||||
+ sta_caps.ant_num = i;
|
||||
+ sta_caps.vht_mcs = vht_mcs_temp + 7;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ sta_caps.vht_su_beamformer = (vht_cap >> 11) & 0x01;
|
||||
+ sta_caps.vht_mu_beamformer = (vht_cap >> 19) & 0x01;
|
||||
+ } else if (sta_caps.ant_num == 0) {
|
||||
+ sta_caps.ant_num = 1;
|
||||
+ }
|
||||
+
|
||||
+ if (sta_caps.vht_ss) {
|
||||
+ sta_caps.wifi_standard = STANDARD_AC;
|
||||
+ } else if (sta_caps.ht_ss) {
|
||||
+ sta_caps.wifi_standard = STANDARD_N;
|
||||
+ } else {
|
||||
+ sta_caps.wifi_standard = STANDARD_A;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void sta_caps_from_rates(const int *supported_rates,
|
||||
+ beerocks::message::sRadioCapabilities &sta_caps)
|
||||
+{
|
||||
+ uint16_t max_rate = 0;
|
||||
+ for (int i = 0; i < 16; i++) {
|
||||
+ uint16_t temp_rate = (supported_rates[i] & 0x7F) * 5; // rate/2 * 10
|
||||
+ if (temp_rate > max_rate) {
|
||||
+ max_rate = temp_rate;
|
||||
+ }
|
||||
+ }
|
||||
+ son::wireless_utils::get_mcs_from_rate(max_rate, beerocks::ANT_MODE_1X1_SS1,
|
||||
+ beerocks::BANDWIDTH_20, sta_caps.default_mcs,
|
||||
+ sta_caps.default_short_gi);
|
||||
+}
|
||||
+
|
||||
+// Parse one hostapd STA-FIRST/STA-NEXT reply block into an association
|
||||
+// notification. Returns nullptr on malformed input. A station that has not
|
||||
+// completed association carries no connected_time line; it is reported
|
||||
+// through @a associated so the caller can skip it but keep iterating.
|
||||
+static std::shared_ptr<char> parse_sta_block(const std::string &reply, int vap_id, bool radio_5G,
|
||||
+ bool &associated, sMacAddr &mac_out)
|
||||
+{
|
||||
+ std::istringstream stream(reply);
|
||||
+ std::string line;
|
||||
+
|
||||
+ if (!std::getline(stream, line)) {
|
||||
+ return nullptr;
|
||||
+ }
|
||||
+ if (!line.empty() && line.back() == '\r') {
|
||||
+ line.pop_back();
|
||||
+ }
|
||||
+ if (!looks_like_mac(line)) {
|
||||
+ return nullptr;
|
||||
+ }
|
||||
+ mac_out = tlvf::mac_from_string(line);
|
||||
+
|
||||
+ std::unordered_map<std::string, std::string> options;
|
||||
+ while (std::getline(stream, line)) {
|
||||
+ if (!line.empty() && line.back() == '\r') {
|
||||
+ line.pop_back();
|
||||
+ }
|
||||
+ auto separator = line.find('=');
|
||||
+ if (separator == std::string::npos) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ options[line.substr(0, separator)] = line.substr(separator + 1);
|
||||
+ }
|
||||
+
|
||||
+ associated = (options.find("connected_time") != options.end());
|
||||
+
|
||||
+ auto msg_buff = ALLOC_SMART_BUFFER(sizeof(sACTION_APMANAGER_CLIENT_ASSOCIATED_NOTIFICATION));
|
||||
+ auto msg = reinterpret_cast<sACTION_APMANAGER_CLIENT_ASSOCIATED_NOTIFICATION *>(msg_buff.get());
|
||||
+ if (!msg) {
|
||||
+ LOG(FATAL) << "Memory allocation failed";
|
||||
+ return nullptr;
|
||||
+ }
|
||||
+ memset(msg_buff.get(), 0, sizeof(sACTION_APMANAGER_CLIENT_ASSOCIATED_NOTIFICATION));
|
||||
+
|
||||
+ msg->params.vap_id = vap_id;
|
||||
+ msg->params.mac = mac_out;
|
||||
+ msg->params.capabilities = {};
|
||||
+
|
||||
+ int supported_rates[16] = {0};
|
||||
+ std::istringstream rates(options["supported_rates"]);
|
||||
+ std::string rate;
|
||||
+ for (int i = 0; i < 16 && (rates >> rate); i++) {
|
||||
+ supported_rates[i] = int(std::strtoul(rate.c_str(), nullptr, 16));
|
||||
+ }
|
||||
+
|
||||
+ int HT_MCS[16] = {0};
|
||||
+ const auto &ht_mcs_line = options["ht_mcs_bitmask"];
|
||||
+ for (size_t i = 0; i < 16 && 2 * i + 1 < ht_mcs_line.length(); i++) {
|
||||
+ HT_MCS[i] = int(std::strtoul(ht_mcs_line.substr(2 * i, 2).c_str(), nullptr, 16));
|
||||
+ }
|
||||
+
|
||||
+ int16_t VHT_MCS[1] = {0};
|
||||
+ const auto &vht_mcs_line = options["rx_vht_mcs_map"];
|
||||
+ if (vht_mcs_line.length() >= 4) {
|
||||
+ VHT_MCS[0] = int16_t(std::strtoul(vht_mcs_line.substr(0, 4).c_str(), nullptr, 16));
|
||||
+ }
|
||||
+
|
||||
+ msg->params.capabilities.max_tx_power =
|
||||
+ uint8_t(std::strtoul(options["max_txpower"].c_str(), nullptr, 10));
|
||||
+
|
||||
+ sta_caps_from_ht(HT_MCS, options["ht_caps_info"], msg->params.capabilities);
|
||||
+ if (radio_5G) {
|
||||
+ sta_caps_from_vht(VHT_MCS, options["vht_caps_info"], msg->params.capabilities);
|
||||
+ }
|
||||
+ sta_caps_from_rates(supported_rates, msg->params.capabilities);
|
||||
+
|
||||
+ return msg_buff;
|
||||
+}
|
||||
+
|
||||
bool ap_wlan_hal_nl80211::generate_connected_clients_events(
|
||||
bool &is_finished_all_clients, std::chrono::steady_clock::time_point max_iteration_timeout)
|
||||
{
|
||||
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED!";
|
||||
+ auto next_unhandled_vap = [this]() {
|
||||
+ for (const auto &vap : m_radio_info.available_vaps) {
|
||||
+ if (m_completed_vaps.find(vap.first) == m_completed_vaps.end()) {
|
||||
+ return vap.first;
|
||||
+ }
|
||||
+ }
|
||||
+ return INVALID_VAP_ID;
|
||||
+ };
|
||||
+
|
||||
+ is_finished_all_clients = false;
|
||||
+
|
||||
+ if (m_vap_id_in_progress == INVALID_VAP_ID) {
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ m_vap_restart_count = 0;
|
||||
+ }
|
||||
+
|
||||
+ while (m_vap_id_in_progress != INVALID_VAP_ID) {
|
||||
+ auto vap_it = m_radio_info.available_vaps.find(m_vap_id_in_progress);
|
||||
+ if (vap_it == m_radio_info.available_vaps.end() || vap_it->second.bss.empty()) {
|
||||
+ m_completed_vaps.insert(m_vap_id_in_progress);
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ continue;
|
||||
+ }
|
||||
+ // On stock OpenWrt the BSS name is the real netdev the control
|
||||
+ // socket is registered under; wlanX.Y spellings do not exist here.
|
||||
+ auto vap_iface = vap_it->second.bss;
|
||||
+
|
||||
+ bool vap_done = false;
|
||||
+ bool vap_enumerated = false;
|
||||
+ while (!vap_done) {
|
||||
+ if (std::chrono::steady_clock::now() > max_iteration_timeout) {
|
||||
+ // Out of time budget: resume from the same cursor next wakeup.
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ std::string cmd = m_queried_first ? "STA-NEXT " + tlvf::mac_to_string(m_prev_client_mac)
|
||||
+ : "STA-FIRST";
|
||||
+ char placeholder = 0;
|
||||
+ char *reply = &placeholder;
|
||||
+ if (!wpa_ctrl_send_msg(cmd, &reply, vap_iface) || !reply) {
|
||||
+ LOG(WARNING) << __func__ << ": '" << cmd << "' failed on " << vap_iface;
|
||||
+ if (m_queried_first && m_vap_restart_count++ < 2) {
|
||||
+ // The cursor station may just have disconnected; restart
|
||||
+ // this VAP - the handled set keeps events deduplicated.
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ return true;
|
||||
+ }
|
||||
+ // A dead control socket must not wedge the whole FSM.
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ std::string text(reply);
|
||||
+ if (text.empty() || text.rfind("FAIL", 0) == 0) {
|
||||
+ if (!text.empty() && m_queried_first && m_vap_restart_count++ < 2) {
|
||||
+ // FAIL for a known cursor: the station disappeared
|
||||
+ // between STA-NEXT calls - restart this VAP once.
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ continue;
|
||||
+ }
|
||||
+ // Only an empty reply is a clean end-of-list; FAIL past the
|
||||
+ // restart budget leaves the VAP only partially walked.
|
||||
+ vap_enumerated = text.empty();
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (text.rfind("UNKNOWN", 0) == 0) {
|
||||
+ LOG(WARNING) << "Station enumeration is unsupported on " << vap_iface;
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ bool associated = false;
|
||||
+ sMacAddr sta_mac;
|
||||
+ auto msg_buff = parse_sta_block(text, m_vap_id_in_progress, get_radio_info().is_5ghz,
|
||||
+ associated, sta_mac);
|
||||
+ if (!msg_buff) {
|
||||
+ LOG(WARNING) << "Malformed station block on " << vap_iface
|
||||
+ << ", skipping the rest of this VAP";
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ m_queried_first = true;
|
||||
+ m_prev_client_mac = sta_mac;
|
||||
+
|
||||
+ if (!m_handled_clients.insert(sta_mac).second) {
|
||||
+ continue; // seen earlier in this sweep
|
||||
+ }
|
||||
+ if (!associated) {
|
||||
+ continue; // authenticated but not associated
|
||||
+ }
|
||||
+
|
||||
+ auto known = m_connected_clients.find(sta_mac);
|
||||
+ if (known != m_connected_clients.end() && known->second == m_vap_id_in_progress) {
|
||||
+ continue; // unchanged client: no duplicate event
|
||||
+ }
|
||||
+ m_connected_clients[sta_mac] = m_vap_id_in_progress;
|
||||
+ LOG(DEBUG) << "Importing connected client " << sta_mac << " on " << vap_iface;
|
||||
+ event_queue_push(Event::STA_Connected, msg_buff);
|
||||
+ }
|
||||
+
|
||||
+ m_completed_vaps.insert(m_vap_id_in_progress);
|
||||
+ if (vap_enumerated) {
|
||||
+ m_enumerated_vaps.insert(m_vap_id_in_progress);
|
||||
+ }
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ m_vap_restart_count = 0;
|
||||
+ }
|
||||
+
|
||||
+ // Reconcile only clients that existed before the sweep, on VAPs whose
|
||||
+ // enumeration reached end-of-list. Runtime events remove their station
|
||||
+ // from the snapshot, preventing a concurrent reconnect from being undone.
|
||||
+ for (const auto &client : m_clients_before_sweep) {
|
||||
+ if (m_enumerated_vaps.find(client.second) == m_enumerated_vaps.end() ||
|
||||
+ m_handled_clients.find(client.first) != m_handled_clients.end()) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ auto current = m_connected_clients.find(client.first);
|
||||
+ if (current == m_connected_clients.end() || current->second != client.second) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ auto msg_buff =
|
||||
+ ALLOC_SMART_BUFFER(sizeof(sACTION_APMANAGER_CLIENT_DISCONNECTED_NOTIFICATION));
|
||||
+ auto msg =
|
||||
+ reinterpret_cast<sACTION_APMANAGER_CLIENT_DISCONNECTED_NOTIFICATION *>(msg_buff.get());
|
||||
+ if (!msg) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ memset(msg_buff.get(), 0, sizeof(sACTION_APMANAGER_CLIENT_DISCONNECTED_NOTIFICATION));
|
||||
+ msg->params.mac = client.first;
|
||||
+ msg->params.vap_id = client.second;
|
||||
+ LOG(DEBUG) << "Client " << client.first
|
||||
+ << " no longer known to hostapd, reporting stale disconnect";
|
||||
+ event_queue_push(Event::STA_Disconnected, msg_buff);
|
||||
+ m_connected_clients.erase(current);
|
||||
+ }
|
||||
+
|
||||
+ m_clients_before_sweep.clear();
|
||||
+ m_enumerated_vaps.clear();
|
||||
+ m_handled_clients.clear();
|
||||
is_finished_all_clients = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ap_wlan_hal_nl80211::pre_generate_connected_clients_events()
|
||||
{
|
||||
-
|
||||
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED!";
|
||||
+ m_vap_id_in_progress = INVALID_VAP_ID;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ m_queried_first = false;
|
||||
+ m_vap_restart_count = 0;
|
||||
+ m_completed_vaps.clear();
|
||||
+ m_enumerated_vaps.clear();
|
||||
+ m_handled_clients.clear();
|
||||
+ m_clients_before_sweep = m_connected_clients;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1427,6 +1754,8 @@ bool ap_wlan_hal_nl80211::process_nl8021
|
||||
}
|
||||
|
||||
// Add the message to the queue
|
||||
+ m_connected_clients[msg->params.mac] = vap_id;
|
||||
+ m_clients_before_sweep.erase(msg->params.mac);
|
||||
event_queue_push(Event::STA_Connected, msg_buff);
|
||||
|
||||
} break;
|
||||
@@ -1453,6 +1782,8 @@ bool ap_wlan_hal_nl80211::process_nl8021
|
||||
msg->params.vap_id = vap_id;
|
||||
msg->params.mac = tlvf::mac_from_string(parsed_obj[bwl::EVENT_KEYLESS_PARAM_MAC]);
|
||||
|
||||
+ m_connected_clients.erase(msg->params.mac);
|
||||
+ m_clients_before_sweep.erase(msg->params.mac);
|
||||
// Add the message to the queue
|
||||
event_queue_push(Event::STA_Disconnected, msg_buff);
|
||||
|
||||
--- a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.h
|
||||
+++ b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.h
|
||||
@@ -12,6 +12,10 @@
|
||||
#include "base_wlan_hal_nl80211.h"
|
||||
#include <bwl/ap_wlan_hal.h>
|
||||
|
||||
+#include <set>
|
||||
+#include <unordered_map>
|
||||
+#include <unordered_set>
|
||||
+
|
||||
namespace bwl {
|
||||
namespace nl80211 {
|
||||
|
||||
@@ -93,6 +97,7 @@ public:
|
||||
* @see ap_wlan_hal::pre_generate_connected_clients_events
|
||||
*/
|
||||
virtual bool pre_generate_connected_clients_events() override;
|
||||
+ virtual bool supports_connected_clients_reconciliation() const override { return true; }
|
||||
|
||||
virtual bool start_wps_pbc() override;
|
||||
virtual bool set_mbo_assoc_disallow(const std::string &bssid, bool enable) override;
|
||||
@@ -140,6 +145,18 @@ protected:
|
||||
}
|
||||
|
||||
private:
|
||||
+ // Connected-clients import and reconciliation (STA-FIRST/STA-NEXT sweep)
|
||||
+ static constexpr int INVALID_VAP_ID = -1;
|
||||
+ std::set<int> m_completed_vaps;
|
||||
+ std::set<int> m_enumerated_vaps;
|
||||
+ std::unordered_set<sMacAddr> m_handled_clients;
|
||||
+ std::unordered_map<sMacAddr, int> m_connected_clients;
|
||||
+ std::unordered_map<sMacAddr, int> m_clients_before_sweep;
|
||||
+ sMacAddr m_prev_client_mac = {};
|
||||
+ bool m_queried_first = false;
|
||||
+ int m_vap_id_in_progress = INVALID_VAP_ID;
|
||||
+ int m_vap_restart_count = 0;
|
||||
+
|
||||
// Unassociated measurement state variables
|
||||
std::chrono::steady_clock::time_point m_unassoc_measure_start;
|
||||
int m_unassoc_measure_window_size = 0;
|
||||
--- a/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.cpp
|
||||
+++ b/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.cpp
|
||||
@@ -27,9 +27,28 @@
|
||||
#include <netlink/msg.h>
|
||||
#include <netlink/netlink.h>
|
||||
|
||||
+#include <cctype>
|
||||
+
|
||||
namespace bwl {
|
||||
namespace nl80211 {
|
||||
|
||||
+bool looks_like_mac(const std::string &text)
|
||||
+{
|
||||
+ if (text.length() != 17) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ for (size_t i = 0; i < text.length(); i++) {
|
||||
+ if (i % 3 == 2) {
|
||||
+ if (text[i] != ':') {
|
||||
+ return false;
|
||||
+ }
|
||||
+ } else if (!std::isxdigit(static_cast<unsigned char>(text[i]))) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////// Local Module Definitions ///////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
--- a/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.h
|
||||
+++ b/common/beerocks/bwl/nl80211/base_wlan_hal_nl80211.h
|
||||
@@ -34,6 +34,9 @@ enum class nl80211_fsm_event { Attach, D
|
||||
constexpr char global_iface[] = "global";
|
||||
constexpr char base_ctrl_path[] = "/var/run/";
|
||||
|
||||
+// Validate the canonical colon-separated MAC format returned by hostapd.
|
||||
+bool looks_like_mac(const std::string &text);
|
||||
+
|
||||
/*!
|
||||
* Base class for the wav abstraction layer.
|
||||
* Read more about virtual inheritance: https://en.wikipedia.org/wiki/Virtual_inheritance
|
||||
--- a/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.cpp
|
||||
+++ b/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.cpp
|
||||
@@ -26,6 +26,7 @@ extern "C" {
|
||||
#include <netlink/genl/genl.h>
|
||||
#include <netlink/msg.h>
|
||||
#include <netlink/netlink.h>
|
||||
+#include <sstream>
|
||||
|
||||
namespace bwl {
|
||||
namespace nl80211 {
|
||||
@@ -457,24 +458,128 @@ bool mon_wlan_hal_nl80211::channel_scan_
|
||||
bool mon_wlan_hal_nl80211::generate_connected_clients_events(
|
||||
bool &is_finished_all_clients, std::chrono::steady_clock::time_point max_iteration_timeout)
|
||||
{
|
||||
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED";
|
||||
- is_finished_all_clients = true;
|
||||
+ auto next_unhandled_vap = [this]() {
|
||||
+ for (const auto &vap : m_radio_info.available_vaps) {
|
||||
+ if (m_completed_vaps.find(vap.first) == m_completed_vaps.end()) {
|
||||
+ return vap.first;
|
||||
+ }
|
||||
+ }
|
||||
+ return INVALID_VAP_ID;
|
||||
+ };
|
||||
+
|
||||
+ is_finished_all_clients = false;
|
||||
+
|
||||
+ if (m_vap_id_in_progress == INVALID_VAP_ID) {
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ m_vap_restart_count = 0;
|
||||
+ }
|
||||
+
|
||||
+ while (m_vap_id_in_progress != INVALID_VAP_ID) {
|
||||
+ auto vap_it = m_radio_info.available_vaps.find(m_vap_id_in_progress);
|
||||
+ if (vap_it == m_radio_info.available_vaps.end() || vap_it->second.bss.empty()) {
|
||||
+ m_completed_vaps.insert(m_vap_id_in_progress);
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ continue;
|
||||
+ }
|
||||
+ // On stock OpenWrt the BSS name is the real netdev the control
|
||||
+ // socket is registered under; wlanX.Y spellings do not exist here.
|
||||
+ auto vap_iface = vap_it->second.bss;
|
||||
+
|
||||
+ bool vap_done = false;
|
||||
+ while (!vap_done) {
|
||||
+ if (std::chrono::steady_clock::now() > max_iteration_timeout) {
|
||||
+ // Out of time budget: resume from the same cursor next wakeup.
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ std::string cmd = m_queried_first ? "STA-NEXT " + tlvf::mac_to_string(m_prev_client_mac)
|
||||
+ : "STA-FIRST";
|
||||
+ char placeholder = 0;
|
||||
+ char *reply = &placeholder;
|
||||
+ if (!wpa_ctrl_send_msg(cmd, &reply, vap_iface) || !reply) {
|
||||
+ LOG(WARNING) << __func__ << ": '" << cmd << "' failed on " << vap_iface;
|
||||
+ if (m_queried_first && m_vap_restart_count++ < 2) {
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ return true;
|
||||
+ }
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ std::string text(reply);
|
||||
+ if (text.empty() || text.rfind("FAIL", 0) == 0) {
|
||||
+ if (!text.empty() && m_queried_first && m_vap_restart_count++ < 2) {
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ continue;
|
||||
+ }
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (text.rfind("UNKNOWN", 0) == 0) {
|
||||
+ LOG(WARNING) << "Station enumeration is unsupported on " << vap_iface;
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
|
||||
- // TODO: implement the API (PPM-1152)
|
||||
- // currently returning true even though not implemented in order not to break
|
||||
- // the flow if this HAL is used by any flow, since the API return value is checked by
|
||||
- // a common flow in the monitor.
|
||||
+ std::istringstream stream(text);
|
||||
+ std::string first_line;
|
||||
+ std::getline(stream, first_line);
|
||||
+ if (!first_line.empty() && first_line.back() == '\r') {
|
||||
+ first_line.pop_back();
|
||||
+ }
|
||||
+ if (!looks_like_mac(first_line)) {
|
||||
+ LOG(WARNING) << "Malformed station block on " << vap_iface
|
||||
+ << ", skipping the rest of this VAP";
|
||||
+ vap_done = true;
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ m_queried_first = true;
|
||||
+ m_prev_client_mac = tlvf::mac_from_string(first_line);
|
||||
+
|
||||
+ if (!m_handled_clients.insert(m_prev_client_mac).second) {
|
||||
+ continue; // seen earlier in this sweep
|
||||
+ }
|
||||
+ if (text.find("connected_time=") == std::string::npos) {
|
||||
+ continue; // authenticated but not associated
|
||||
+ }
|
||||
+
|
||||
+ auto msg_buff =
|
||||
+ ALLOC_SMART_BUFFER(sizeof(sACTION_MONITOR_CLIENT_ASSOCIATED_NOTIFICATION));
|
||||
+ auto msg =
|
||||
+ reinterpret_cast<sACTION_MONITOR_CLIENT_ASSOCIATED_NOTIFICATION *>(msg_buff.get());
|
||||
+ if (!msg) {
|
||||
+ LOG(FATAL) << "Memory allocation failed";
|
||||
+ return false;
|
||||
+ }
|
||||
+ memset(msg_buff.get(), 0, sizeof(sACTION_MONITOR_CLIENT_ASSOCIATED_NOTIFICATION));
|
||||
+ msg->vap_id = m_vap_id_in_progress;
|
||||
+ msg->mac = m_prev_client_mac;
|
||||
+ event_queue_push(Event::STA_Connected, msg_buff);
|
||||
+ }
|
||||
+
|
||||
+ m_completed_vaps.insert(m_vap_id_in_progress);
|
||||
+ m_queried_first = false;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ m_vap_id_in_progress = next_unhandled_vap();
|
||||
+ m_vap_restart_count = 0;
|
||||
+ }
|
||||
+
|
||||
+ m_handled_clients.clear();
|
||||
+ is_finished_all_clients = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mon_wlan_hal_nl80211::pre_generate_connected_clients_events()
|
||||
{
|
||||
- LOG(TRACE) << __func__ << " - NOT IMPLEMENTED";
|
||||
-
|
||||
- // TODO: implement the API (PPM-1152)
|
||||
- // currently returning true even though not implemented in order not to break
|
||||
- // the flow if this HAL is used by any flow, since the API return value is checked by
|
||||
- // a common flow in the monitor.
|
||||
+ m_vap_id_in_progress = INVALID_VAP_ID;
|
||||
+ m_prev_client_mac = beerocks::net::network_utils::ZERO_MAC;
|
||||
+ m_queried_first = false;
|
||||
+ m_vap_restart_count = 0;
|
||||
+ m_completed_vaps.clear();
|
||||
+ m_handled_clients.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
--- a/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.h
|
||||
+++ b/common/beerocks/bwl/nl80211/mon_wlan_hal_nl80211.h
|
||||
@@ -12,6 +12,9 @@
|
||||
#include "base_wlan_hal_nl80211.h"
|
||||
#include <bwl/mon_wlan_hal.h>
|
||||
|
||||
+#include <set>
|
||||
+#include <unordered_set>
|
||||
+
|
||||
namespace bwl {
|
||||
namespace nl80211 {
|
||||
|
||||
@@ -88,6 +91,15 @@ protected:
|
||||
|
||||
// Private data-members:
|
||||
private:
|
||||
+ // Connected-clients import (STA-FIRST/STA-NEXT sweep)
|
||||
+ static constexpr int INVALID_VAP_ID = -1;
|
||||
+ std::set<int> m_completed_vaps;
|
||||
+ std::unordered_set<sMacAddr> m_handled_clients;
|
||||
+ sMacAddr m_prev_client_mac = {};
|
||||
+ bool m_queried_first = false;
|
||||
+ int m_vap_id_in_progress = INVALID_VAP_ID;
|
||||
+ int m_vap_restart_count = 0;
|
||||
+
|
||||
std::shared_ptr<char> m_temp_wav_value;
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
From 6da943b44dee4dc3cc51d4b4b1dfc7c68069b9e5 Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Wed, 29 Jul 2026 21:05:00 +0300
|
||||
Subject: [PATCH] logging: use production levels by default
|
||||
|
||||
Debug and trace logging make normal link-metrics traffic dominate the
|
||||
controller log on stock OpenWrt. Keep informational, warning, error and
|
||||
fatal diagnostics, while leaving verbose levels available for an
|
||||
explicit troubleshooting session.
|
||||
|
||||
Apply the same production default to file and syslog output for both the
|
||||
controller and agent so enabling syslog later cannot reintroduce the
|
||||
same amplification.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
agent/config/beerocks_agent.conf.in | 4 ++--
|
||||
controller/config/beerocks_controller.conf.in | 4 ++--
|
||||
2 files changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
--- a/agent/config/beerocks_agent.conf.in
|
||||
+++ b/agent/config/beerocks_agent.conf.in
|
||||
@@ -92,8 +92,8 @@ enable_repeater_mode=0
|
||||
em_handle_third_party=0
|
||||
|
||||
[log]
|
||||
-log_global_levels=error,info,warning,fatal,trace,debug
|
||||
-log_global_syslog_levels=error,info,warning,fatal,trace,debug
|
||||
+log_global_levels=error,info,warning,fatal
|
||||
+log_global_syslog_levels=error,info,warning,fatal
|
||||
log_global_size=@BEEROCKS_AGENT_LOG_SIZE@
|
||||
log_files_enabled=@BEEROCKS_LOG_FILES_ENABLED@
|
||||
log_files_path=@BEEROCKS_LOG_FILES_PATH@
|
||||
--- a/controller/config/beerocks_controller.conf.in
|
||||
+++ b/controller/config/beerocks_controller.conf.in
|
||||
@@ -56,8 +56,8 @@ fail_safe_5G_vht_frequency=5210
|
||||
use_dataelements_vap_configs=@BEEROCKS_CONTROLLER_DATAELEMENTS_VAP_CONFIGS@
|
||||
|
||||
[log]
|
||||
-log_global_levels=error,info,warning,fatal,trace,debug
|
||||
-log_global_syslog_levels=error,info,warning,fatal,trace,debug
|
||||
+log_global_levels=error,info,warning,fatal
|
||||
+log_global_syslog_levels=error,info,warning,fatal
|
||||
log_global_size=@BEEROCKS_CONTROLLER_LOG_SIZE@
|
||||
log_files_enabled=@BEEROCKS_LOG_FILES_ENABLED@
|
||||
log_files_path=@BEEROCKS_LOG_FILES_PATH@
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
From 4f7a47abe79317fa4be0cb5d4d46705dd70a6758 Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Thu, 30 Jul 2026 12:00:00 +0300
|
||||
Subject: [PATCH] scripts: accept the openwrt target platform in prplmesh_utils
|
||||
|
||||
The helper's platform guard accepted only linux|rdkb, so a helper
|
||||
generated with TARGET_PLATFORM=openwrt exited with "Unsupported
|
||||
platform: openwrt" before roll_logs could enforce log retention.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
common/beerocks/scripts/prplmesh_utils.sh.in | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
--- a/common/beerocks/scripts/prplmesh_utils.sh.in
|
||||
+++ b/common/beerocks/scripts/prplmesh_utils.sh.in
|
||||
@@ -165,7 +165,7 @@ stop_func() {
|
||||
|
||||
main() {
|
||||
case "${TARGET_PLATFORM}" in
|
||||
- linux|rdkb)
|
||||
+ linux|rdkb|openwrt)
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported platform: ${TARGET_PLATFORM}" >&2
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
From e362259e3b40c451410ae4d2a100d7750f4a2883 Mon Sep 17 00:00:00 2001
|
||||
From: Andrei Ovcharenko <a@krot.name>
|
||||
Date: Wed, 29 Jul 2026 22:35:00 +0300
|
||||
Subject: [PATCH] agent: ignore inactive wireless backhaul links
|
||||
|
||||
The backhaul link list stores credentials for every wireless-capable
|
||||
radio, including radios that are not currently used for backhaul. Link
|
||||
metrics reporting treated every entry as an active wireless backhaul
|
||||
and warned whenever an AP-only radio had no backhaul interface.
|
||||
|
||||
Skip those expected inactive entries without a warning. Keep the
|
||||
warning when the selected active wireless backhaul is missing its
|
||||
interface, and handle a stale radio entry without dereferencing it.
|
||||
|
||||
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 <a@krot.name>
|
||||
---
|
||||
.../slave/tasks/link_metrics_collection_task.cpp | 12 +++++++++++-
|
||||
1 file changed, 11 insertions(+), 1 deletion(-)
|
||||
|
||||
--- a/agent/src/beerocks/slave/tasks/link_metrics_collection_task.cpp
|
||||
+++ b/agent/src/beerocks/slave/tasks/link_metrics_collection_task.cpp
|
||||
@@ -1724,8 +1724,18 @@ bool LinkMetricsCollectionTask::get_neig
|
||||
|
||||
for (const auto &bh_wifi_info : db->backhaul.backhaul_links) {
|
||||
auto radio = db->radio(bh_wifi_info.iface_name);
|
||||
+ if (!radio) {
|
||||
+ LOG(WARNING) << "Backhaul radio " << bh_wifi_info.iface_name << " not found";
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
if (radio->back.iface_name.empty()) {
|
||||
- LOG(WARNING) << "Found empty backhaul link name!";
|
||||
+ // backhaul_links also contains credentials for radios that are not currently
|
||||
+ // used for wireless backhaul. An empty back interface is expected for them.
|
||||
+ if (db->backhaul.connection_type == AgentDB::sBackhaul::eConnectionType::Wireless &&
|
||||
+ db->backhaul.selected_iface_name == bh_wifi_info.iface_name) {
|
||||
+ LOG(WARNING) << "Found empty backhaul link name!";
|
||||
+ }
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
# shellcheck shell=busybox
|
||||
|
||||
set -eu
|
||||
|
||||
case "$PKG_NAME" in
|
||||
prplmesh)
|
||||
grep -F "prplmesh_version=$PKG_VERSION" /usr/share/prplmesh/config/version
|
||||
;;
|
||||
*)
|
||||
echo "Untested package: $PKG_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
|
||||
# shellcheck shell=busybox
|
||||
|
||||
set -eu
|
||||
|
||||
case "$PKG_NAME" in
|
||||
prplmesh)
|
||||
for config in \
|
||||
/usr/share/prplmesh/config/beerocks_agent.conf \
|
||||
/usr/share/prplmesh/config/beerocks_controller.conf; do
|
||||
grep -Fx 'log_global_levels=error,info,warning,fatal' "$config"
|
||||
grep -Fx 'log_global_syslog_levels=error,info,warning,fatal' "$config"
|
||||
done
|
||||
# -h reaches "usage; exit 0" only after main()'s platform guard has
|
||||
# accepted TARGET_PLATFORM=openwrt; an unpatched helper exits 1 with
|
||||
# "Unsupported platform: openwrt" instead, so this self-tests patch
|
||||
# 161.
|
||||
/usr/libexec/prplmesh/scripts/prplmesh_utils.sh -h
|
||||
;;
|
||||
*)
|
||||
echo "Untested package: $PKG_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue