From 918ec01a76995deca767c5bc98e32de3573b1385 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sun, 26 Jul 2026 21:23:07 +0300
Subject: [PATCH] bwl: add bundled minimal wpa_ctrl client
The NL80211 flavor compiles wpa_ctrl.c and os_unix.c straight out of a
hostapd source tree, so every integration has to carry a hostapd
checkout at build time even when the target only talks to the control
sockets of an already installed hostapd.
Add a small self-contained implementation of the documented control
interface protocol with the same API and semantics as hostap's
wpa_ctrl.h (solicited replies with a 10 s timeout and -2 on expiry,
event filtering while waiting, ATTACH/DETACH, pending/recv/get_fd), and
a BWL_BUNDLED_WPA_CTRL option (default OFF) that builds it instead of
the hostapd sources. The client socket is created group accessible for
user/group network so a hostapd jailed the way OpenWrt does it can
reply, which is what the OpenWrt tree patches into wpa_ctrl.c.
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
---
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
+
+#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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* Directory for the local (client) end of the connection. The daemon only
+ * replies to a bound datagram socket, so every connection creates one. */
+#define WPA_CTRL_CLIENT_DIR "/tmp"
+
+/* How long wpa_ctrl_request() waits for a solicited reply. Same value as
+ * the reference client so BWL retry counting keeps its meaning. */
+#define WPA_CTRL_REPLY_TIMEOUT_SEC 10
+
+/* OpenWrt runs hostapd inside a procd jail as user/group "network" while
+ * BWL usually runs as root. The daemon must be able to send datagrams to
+ * our client socket, so the socket node is made group accessible and
+ * handed over to that account (statically allocated as 101 on OpenWrt,
+ * resolved by name when possible). */
+#define WPA_CTRL_DAEMON_FALLBACK_ID 101
+
+struct wpa_ctrl {
+ int s;
+ struct sockaddr_un local;
+ struct sockaddr_un dest;
+};
+
+static void wpa_ctrl_grant_daemon_access(const char *path)
+{
+ struct group *grp = getgrnam("network");
+ struct passwd *pwd = getpwnam("network");
+ gid_t gid = grp ? grp->gr_gid : (gid_t)WPA_CTRL_DAEMON_FALLBACK_ID;
+ uid_t uid = pwd ? pwd->pw_uid : (uid_t)WPA_CTRL_DAEMON_FALLBACK_ID;
+
+ /* Group first so restricted processes still hand over what they can,
+ * 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; }