* [PATCH 4/7] wifi: brcmfmac: create and remove the AWDL interface
2026-11-04 15:15 [PATCH 0/7] wifi: brcmfmac: AWDL for AirDrop on Apple T2 Macs Andre Eikmeyer
` (2 preceding siblings ...)
2026-11-04 15:15 ` [PATCH 3/7] wifi: brcmfmac: detect AWDL support of the firmware Andre Eikmeyer
@ 2026-11-04 15:15 ` Andre Eikmeyer
2026-11-04 15:15 ` [PATCH 5/7] wifi: brcmfmac: add the AWDL data path Andre Eikmeyer
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Andre Eikmeyer @ 2026-11-04 15:15 UTC (permalink / raw)
To: arend.vanspriel
Cc: linux-wireless, brcm80211, brcm80211-dev-list.pdl, linux-kernel,
Andre Eikmeyer, Yossef Osman, Aditya Garg
Firmware with AWDL support takes an interface of type AWDL from
interface_create. The host gives the MAC address, the firmware
interface index 2 and the AWDL BSSID 00:25:00:ff:94:73. The firmware
answers with an interface event like for any other interface.
The AWDL interface lives outside cfg80211. Add brcmf_awdl_add() and
brcmf_awdl_del(), called with RTNL and the wiphy mutex held, which
create and remove it together with a network device "awdl%d":
- The interface events of the AWDL interface complete a wait in
brcmf_awdl_add() and brcmf_awdl_del() instead of going to the
cfg80211 handler. A vif_event armed for a P2P or AP interface at the
same time stays with that interface. When the firmware removes the
AWDL interface on its own, the event worker removes it as before.
- The network device gets its own net_device_ops with open, stop and
start_xmit before it is registered. The firmware filters AWDL traffic
itself.
- brcmf_net_attach() and brcmf_net_detach() also register and
unregister a network device outside cfg80211 with RTNL held.
- brcmf_awdl_add() needs the AWDL feature and a free firmware
interface index 2.
- The AWDL interface uses direct addressing like an AP, every peer
gets its own flowrings. brcmf_awdl_del() deletes them before it
stops AWDL and removes the interface.
- The AWDL interface is removed in the cfg80211 suspend callback while
the firmware still takes commands.
Tested-by: Yossef Osman <Yossef.Osman@proton.me>
Tested-by: Aditya Garg <aditya.garg@linux.dev>
Signed-off-by: Andre Eikmeyer <dev@deq.rocks>
---
.../broadcom/brcm80211/brcmfmac/Makefile | 3 +-
.../broadcom/brcm80211/brcmfmac/awdl.c | 268 ++++++++++++++++++
.../broadcom/brcm80211/brcmfmac/awdl.h | 47 +++
.../broadcom/brcm80211/brcmfmac/cfg80211.c | 6 +
.../broadcom/brcm80211/brcmfmac/core.c | 59 +++-
.../broadcom/brcm80211/brcmfmac/core.h | 7 +
.../broadcom/brcm80211/brcmfmac/fweh.c | 17 ++
7 files changed, 398 insertions(+), 9 deletions(-)
create mode 100644 drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
create mode 100644 drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/Makefile b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/Makefile
index e5ca0f51182..36c634199a2 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/Makefile
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/Makefile
@@ -25,7 +25,8 @@ brcmfmac-objs += \
btcoex.o \
vendor.o \
pno.o \
- xtlv.o
+ xtlv.o \
+ awdl.o
brcmfmac-$(CONFIG_BRCMFMAC_PROTO_BCDC) += \
bcdc.o \
fwsignal.o
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
new file mode 100644
index 00000000000..485ed74f3d6
--- /dev/null
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 André Eikmeyer <dev@deq.rocks>
+ *
+ * AWDL interface of the Apple firmware of the BCM4364 and BCM4377.
+ *
+ * The firmware runs Apple Wireless Direct Link itself once the host creates
+ * an interface of type AWDL with interface_create and configures it through
+ * the awdl_* iovars. The AWDL interface has a network device but no cfg80211
+ * vif. Its interface events are handled here and not by cfg80211.
+ */
+
+#include <linux/etherdevice.h>
+#include <linux/netdevice.h>
+#include <linux/rtnetlink.h>
+
+#include "core.h"
+#include "debug.h"
+#include "feature.h"
+#include "fwil.h"
+#include "fwil_types.h"
+#include "proto.h"
+#include "awdl.h"
+
+/* The firmware interface index of the AWDL interface */
+#define BRCMF_AWDL_IFIDX 2
+#define BRCMF_AWDL_IF_TIMEOUT msecs_to_jiffies(2000)
+
+/* The BSSID of all AWDL frames */
+static const u8 brcmf_awdl_bssid[ETH_ALEN] = {
+ 0x00, 0x25, 0x00, 0xff, 0x94, 0x73,
+};
+
+void brcmf_awdl_attach(struct brcmf_pub *drvr)
+{
+ spin_lock_init(&drvr->awdl.lock);
+ init_completion(&drvr->awdl.if_event);
+}
+
+/*
+ * Called from the firmware event worker after an interface was added. Takes
+ * the interface if it is the AWDL interface being created.
+ */
+bool brcmf_awdl_if_added(struct brcmf_pub *drvr, struct brcmf_if *ifp)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ bool own;
+
+ spin_lock(&awdl->lock);
+ own = awdl->adding && ifp->ndev &&
+ ether_addr_equal(ifp->mac_addr, awdl->mac);
+ if (own) {
+ ifp->awdl = true;
+ strscpy(ifp->ndev->name, "awdl%d", IFNAMSIZ);
+ awdl->ifp = ifp;
+ awdl->adding = false;
+ complete(&awdl->if_event);
+ }
+ spin_unlock(&awdl->lock);
+ return own;
+}
+
+/*
+ * Called from the firmware event worker before an interface is removed.
+ * Returns true if the removal of the AWDL interface was requested by
+ * brcmf_awdl_del(), which then removes it. When the firmware removes the
+ * AWDL interface on its own, it is forgotten here and removed by the caller.
+ */
+bool brcmf_awdl_if_removed(struct brcmf_pub *drvr, struct brcmf_if *ifp)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ bool requested = false;
+
+ if (!ifp->awdl)
+ return false;
+
+ spin_lock(&awdl->lock);
+ if (awdl->ifp == ifp) {
+ requested = awdl->removing;
+ if (requested)
+ complete(&awdl->if_event);
+ else
+ awdl->ifp = NULL;
+ }
+ spin_unlock(&awdl->lock);
+ if (!requested)
+ brcmf_proto_configure_addr_mode(drvr, ifp->ifidx,
+ ADDR_INDIRECT);
+ return requested;
+}
+
+/* The firmware interface index of the AWDL interface must be free */
+static bool brcmf_awdl_ifidx_used(struct brcmf_pub *drvr)
+{
+ int i;
+
+ for (i = 1; i < BRCMF_MAX_IFS; i++)
+ if (drvr->iflist[i] && drvr->iflist[i]->ifidx == BRCMF_AWDL_IFIDX)
+ return true;
+ return false;
+}
+
+/* Undoes interface_create for an interface that was not attached */
+static void brcmf_awdl_drop_if(struct brcmf_pub *drvr, struct brcmf_if *ifp)
+{
+ struct net_device *ndev = ifp->ndev;
+
+ brcmf_fil_iovar_data_set(ifp, "interface_remove", NULL, 0);
+ brcmf_proto_configure_addr_mode(drvr, ifp->ifidx, ADDR_INDIRECT);
+ brcmf_proto_del_if(drvr, ifp);
+ if (drvr->iflist[ifp->bsscfgidx] == ifp)
+ drvr->iflist[ifp->bsscfgidx] = NULL;
+ if (drvr->if2bss[ifp->ifidx] == ifp->bsscfgidx)
+ drvr->if2bss[ifp->ifidx] = BRCMF_BSSIDX_INVALID;
+ free_netdev(ndev);
+}
+
+/**
+ * brcmf_awdl_add() - create the AWDL interface.
+ *
+ * @drvr: device.
+ * @mac: MAC address of the AWDL interface.
+ * @ndev: set to the network device of the AWDL interface.
+ *
+ * Called with RTNL and the wiphy mutex held. The interface uses the firmware interface index 2
+ * and the AWDL BSSID 00:25:00:ff:94:73, which the host passes to the
+ * firmware. Return: 0 or a negative error code.
+ */
+int brcmf_awdl_add(struct brcmf_pub *drvr, const u8 *mac,
+ struct net_device **ndev)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
+ struct wl_interface_create_v3 req = {};
+ struct brcmf_if *ifp;
+ long left = 0;
+ int err;
+
+ ASSERT_RTNL();
+ lockdep_assert_wiphy(drvr->wiphy);
+
+ if (!pri || !brcmf_feat_is_enabled(pri, BRCMF_FEAT_AWDL))
+ return -EOPNOTSUPP;
+ if (!is_valid_ether_addr(mac))
+ return -EINVAL;
+ if (awdl->detached)
+ return -ENODEV;
+ if (awdl->ifp)
+ return -EBUSY;
+ if (brcmf_awdl_ifidx_used(drvr)) {
+ bphy_err(drvr, "interface index %d is in use, no AWDL\n",
+ BRCMF_AWDL_IFIDX);
+ return -EBUSY;
+ }
+
+ spin_lock(&awdl->lock);
+ ether_addr_copy(awdl->mac, mac);
+ awdl->adding = true;
+ reinit_completion(&awdl->if_event);
+ spin_unlock(&awdl->lock);
+
+ req.ver = WL_INTERFACE_CREATE_VER_3;
+ req.len = sizeof(req);
+ req.fixed_len = sizeof(req);
+ req.iftype = WL_INTERFACE_CREATE_AWDL;
+ req.flags = WL_INTERFACE_MAC_USE | WL_INTERFACE_IF_INDEX_USE |
+ WL_INTERFACE_BSSID_USE;
+ ether_addr_copy(req.mac_addr, mac);
+ ether_addr_copy(req.bssid, brcmf_awdl_bssid);
+ req.if_index = BRCMF_AWDL_IFIDX;
+ err = brcmf_fil_iovar_data_get(pri, "interface_create", &req,
+ sizeof(req));
+ if (!err)
+ left = wait_for_completion_timeout(&awdl->if_event,
+ BRCMF_AWDL_IF_TIMEOUT);
+
+ spin_lock(&awdl->lock);
+ awdl->adding = false;
+ ifp = awdl->ifp;
+ spin_unlock(&awdl->lock);
+
+ if (err) {
+ bphy_err(drvr, "AWDL interface_create failed: %d\n", err);
+ return err;
+ }
+ if (!left || !ifp) {
+ bphy_err(drvr, "no interface event for the AWDL interface\n");
+ return -ETIMEDOUT;
+ }
+
+ /* Like an AP, AWDL sends to each peer through its own flowrings */
+ brcmf_proto_configure_addr_mode(drvr, ifp->ifidx, ADDR_DIRECT);
+ err = brcmf_net_attach(ifp, true);
+ if (err) {
+ spin_lock(&awdl->lock);
+ awdl->ifp = NULL;
+ spin_unlock(&awdl->lock);
+ brcmf_awdl_drop_if(drvr, ifp);
+ return err;
+ }
+ brcmf_dbg(INFO, "%s: AWDL interface, bsscfgidx %d ifidx %d\n",
+ ifp->ndev->name, ifp->bsscfgidx, ifp->ifidx);
+ *ndev = ifp->ndev;
+ return 0;
+}
+
+/**
+ * brcmf_awdl_del() - remove the AWDL interface.
+ *
+ * @drvr: device.
+ *
+ * Called with RTNL and the wiphy mutex held. Deletes the flowrings of the AWDL interface, stops
+ * AWDL and removes the interface in the firmware and on the host.
+ * Return: 0 or a negative error code.
+ */
+int brcmf_awdl_del(struct brcmf_pub *drvr)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ struct brcmf_if *ifp;
+ int err;
+
+ ASSERT_RTNL();
+ lockdep_assert_wiphy(drvr->wiphy);
+
+ ifp = awdl->ifp;
+ if (!ifp)
+ return -ENOENT;
+
+ netif_tx_disable(ifp->ndev);
+ /* Deletes the flowrings of all peers */
+ brcmf_proto_configure_addr_mode(drvr, ifp->ifidx, ADDR_INDIRECT);
+ brcmf_fil_iovar_int_set(ifp, "awdl", 0);
+
+ spin_lock(&awdl->lock);
+ awdl->removing = true;
+ reinit_completion(&awdl->if_event);
+ spin_unlock(&awdl->lock);
+
+ err = brcmf_fil_iovar_data_set(ifp, "interface_remove", NULL, 0);
+ if (!err && !wait_for_completion_timeout(&awdl->if_event,
+ BRCMF_AWDL_IF_TIMEOUT)) {
+ bphy_err(drvr, "no interface event for the AWDL removal\n");
+ err = -ETIMEDOUT;
+ }
+
+ spin_lock(&awdl->lock);
+ awdl->removing = false;
+ awdl->ifp = NULL;
+ spin_unlock(&awdl->lock);
+
+ brcmf_remove_interface(ifp, true);
+ return err;
+}
+
+/* Called before the interfaces of the device are removed */
+void brcmf_awdl_detach(struct brcmf_pub *drvr)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+
+ rtnl_lock();
+ wiphy_lock(drvr->wiphy);
+ spin_lock(&awdl->lock);
+ awdl->detached = true;
+ awdl->ifp = NULL;
+ spin_unlock(&awdl->lock);
+ wiphy_unlock(drvr->wiphy);
+ rtnl_unlock();
+}
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
new file mode 100644
index 00000000000..b12cbd32187
--- /dev/null
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026 André Eikmeyer <dev@deq.rocks>
+ *
+ * AWDL interface of the Apple firmware, see awdl.c
+ */
+#ifndef BRCMFMAC_AWDL_H
+#define BRCMFMAC_AWDL_H
+
+#include <linux/completion.h>
+#include <linux/if_ether.h>
+#include <linux/spinlock.h>
+
+struct brcmf_pub;
+struct brcmf_if;
+
+/**
+ * struct brcmf_awdl - AWDL interface of a device.
+ *
+ * @lock: protects @mac, @adding, @removing and @ifp against the firmware
+ * event worker. Changes of @ifp also hold RTNL and the wiphy mutex.
+ * @if_event: completed by the interface event of the AWDL interface.
+ * @mac: MAC address requested for the AWDL interface.
+ * @adding: interface_create was sent, its interface event is awaited.
+ * @removing: interface_remove was sent, its interface event is awaited.
+ * @detached: the device goes away, no new AWDL interface is created.
+ * @ifp: the AWDL interface, NULL when there is none.
+ */
+struct brcmf_awdl {
+ spinlock_t lock; /* AWDL state against the event worker */
+ struct completion if_event;
+ u8 mac[ETH_ALEN];
+ bool adding;
+ bool removing;
+ bool detached;
+ struct brcmf_if *ifp;
+};
+
+void brcmf_awdl_attach(struct brcmf_pub *drvr);
+void brcmf_awdl_detach(struct brcmf_pub *drvr);
+int brcmf_awdl_add(struct brcmf_pub *drvr, const u8 *mac,
+ struct net_device **ndev);
+int brcmf_awdl_del(struct brcmf_pub *drvr);
+bool brcmf_awdl_if_added(struct brcmf_pub *drvr, struct brcmf_if *ifp);
+bool brcmf_awdl_if_removed(struct brcmf_pub *drvr, struct brcmf_if *ifp);
+
+#endif /* BRCMFMAC_AWDL_H */
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
index 1037abe6d58..4f35dbef082 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
@@ -4279,6 +4279,12 @@ static s32 brcmf_cfg80211_suspend(struct wiphy *wiphy,
brcmf_dbg(TRACE, "Enter\n");
+ /* The AWDL interface does not survive sleep, the firmware still
+ * takes commands here. User space creates it again after resume.
+ */
+ if (cfg->pub->awdl.ifp)
+ brcmf_awdl_del(cfg->pub);
+
/* if the primary net_device is not READY there is nothing
* we can do but pray resume goes smoothly.
*/
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c
index 92ec269f43b..37c558b6f0d 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c
@@ -32,8 +32,6 @@
#define MAX_WAIT_FOR_8021X_TX msecs_to_jiffies(950)
-#define BRCMF_BSSIDX_INVALID -1
-
#define RXS_PBPRES BIT(2)
#define D11_PHY_HDR_LEN 6
@@ -642,6 +640,42 @@ static const struct net_device_ops brcmf_netdev_ops_pri = {
.ndo_set_rx_mode = brcmf_netdev_set_multicast_list
};
+static int brcmf_awdl_netdev_open(struct net_device *ndev)
+{
+ struct brcmf_if *ifp = netdev_priv(ndev);
+
+ if (ifp->drvr->bus_if->state != BRCMF_BUS_UP)
+ return -EAGAIN;
+
+ /* The firmware runs AWDL, the link is there while it exists */
+ netif_carrier_on(ndev);
+ netif_start_queue(ndev);
+ return 0;
+}
+
+static int brcmf_awdl_netdev_stop(struct net_device *ndev)
+{
+ netif_stop_queue(ndev);
+ netif_carrier_off(ndev);
+ return 0;
+}
+
+/*
+ * The AWDL interface gets no multicast list and no MAC address change, the
+ * firmware filters AWDL traffic itself.
+ */
+static const struct net_device_ops brcmf_netdev_ops_awdl = {
+ .ndo_open = brcmf_awdl_netdev_open,
+ .ndo_stop = brcmf_awdl_netdev_stop,
+ .ndo_start_xmit = brcmf_netdev_start_xmit,
+};
+
+static bool brcmf_netdev_is_brcmf(struct net_device *ndev)
+{
+ return ndev->netdev_ops == &brcmf_netdev_ops_pri ||
+ ndev->netdev_ops == &brcmf_netdev_ops_awdl;
+}
+
int brcmf_net_attach(struct brcmf_if *ifp, bool locked)
{
struct brcmf_pub *drvr = ifp->drvr;
@@ -653,7 +687,10 @@ int brcmf_net_attach(struct brcmf_if *ifp, bool locked)
ndev = ifp->ndev;
/* set appropriate operations */
- ndev->netdev_ops = &brcmf_netdev_ops_pri;
+ if (ifp->awdl)
+ ndev->netdev_ops = &brcmf_netdev_ops_awdl;
+ else
+ ndev->netdev_ops = &brcmf_netdev_ops_pri;
ndev->needed_headroom += drvr->hdrlen;
ndev->ethtool_ops = &brcmf_ethtool_ops;
@@ -665,10 +702,12 @@ int brcmf_net_attach(struct brcmf_if *ifp, bool locked)
INIT_WORK(&ifp->multicast_work, _brcmf_set_multicast_list);
INIT_WORK(&ifp->ndoffload_work, _brcmf_update_ndtable);
- if (locked)
+ if (!locked)
+ err = register_netdev(ndev);
+ else if (ndev->ieee80211_ptr)
err = cfg80211_register_netdevice(ndev);
else
- err = register_netdev(ndev);
+ err = register_netdevice(ndev);
if (err != 0) {
bphy_err(drvr, "couldn't register the net device\n");
goto fail;
@@ -689,10 +728,12 @@ int brcmf_net_attach(struct brcmf_if *ifp, bool locked)
void brcmf_net_detach(struct net_device *ndev, bool locked)
{
if (ndev->reg_state == NETREG_REGISTERED) {
- if (locked)
+ if (!locked)
+ unregister_netdev(ndev);
+ else if (ndev->ieee80211_ptr)
cfg80211_unregister_netdevice(ndev);
else
- unregister_netdev(ndev);
+ unregister_netdevice(ndev);
} else {
brcmf_cfg80211_free_vif(ndev);
free_netdev(ndev);
@@ -943,7 +984,7 @@ static void brcmf_del_if(struct brcmf_pub *drvr, s32 bsscfgidx,
netif_stop_queue(ifp->ndev);
}
- if (ifp->ndev->netdev_ops == &brcmf_netdev_ops_pri) {
+ if (brcmf_netdev_is_brcmf(ifp->ndev)) {
cancel_work_sync(&ifp->multicast_work);
cancel_work_sync(&ifp->ndoffload_work);
}
@@ -1371,6 +1412,7 @@ int brcmf_attach(struct device *dev)
drvr->if2bss[i] = BRCMF_BSSIDX_INVALID;
mutex_init(&drvr->proto_block);
+ brcmf_awdl_attach(drvr);
/* Link to bus module */
drvr->hdrlen = 0;
@@ -1486,6 +1528,7 @@ void brcmf_detach(struct device *dev)
unregister_inet6addr_notifier(&drvr->inet6addr_notifier);
#endif
+ brcmf_awdl_detach(drvr);
brcmf_bus_change_state(bus_if, BRCMF_BUS_DOWN);
/* make sure primary interface removed last */
for (i = BRCMF_MAX_IFS - 1; i > -1; i--) {
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.h
index 399b6810e39..fc8a713e80c 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.h
@@ -12,6 +12,7 @@
#include <net/cfg80211.h>
#include "fweh.h"
+#include "awdl.h"
#if IS_MODULE(CONFIG_BRCMFMAC)
#define BRCMF_EXPORT_SYMBOL_GPL(__sym) EXPORT_SYMBOL_NS_GPL(__sym, "BRCMFMAC")
@@ -25,6 +26,8 @@
/* For supporting multiple interfaces */
#define BRCMF_MAX_IFS 16
+#define BRCMF_BSSIDX_INVALID -1
+
/* Small, medium and maximum buffer size for dcmd
*/
#define BRCMF_DCMD_SMLEN 256
@@ -145,6 +148,8 @@ struct brcmf_pub {
u8 sta_mac_idx;
const struct brcmf_fwvid_ops *vops;
void *vdata;
+
+ struct brcmf_awdl awdl;
};
/* forward declarations */
@@ -184,6 +189,7 @@ enum brcmf_netif_stop_reason {
* @pend_8021x_cnt: tracks outstanding number of 802.1x frames.
* @pend_8021x_wait: used for signalling change in count.
* @fwil_fwerr: flag indicating fwil layer should return firmware error codes.
+ * @awdl: this is the AWDL interface, it has no cfg80211 vif.
*/
struct brcmf_if {
struct brcmf_pub *drvr;
@@ -202,6 +208,7 @@ struct brcmf_if {
struct in6_addr ipv6_addr_tbl[NDOL_MAX_ENTRIES];
u8 ipv6addr_idx;
bool fwil_fwerr;
+ bool awdl;
};
int brcmf_netdev_wait_pend8021x(struct brcmf_if *ifp);
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
index 1cff4ba7694..8c098029104 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
@@ -170,6 +170,9 @@ static void brcmf_fweh_handle_if_event(struct brcmf_pub *drvr,
return;
if (!is_p2pdev)
brcmf_proto_add_if(drvr, ifp);
+ /* The AWDL interface is attached by brcmf_awdl_add() */
+ if (brcmf_awdl_if_added(drvr, ifp))
+ return;
if (!drvr->fweh->evt_handler[BRCMF_E_IF])
if (brcmf_net_attach(ifp, false) < 0)
return;
@@ -178,6 +181,14 @@ static void brcmf_fweh_handle_if_event(struct brcmf_pub *drvr,
if (ifp && ifevent->action == BRCMF_E_IF_CHANGE)
brcmf_proto_reset_if(drvr, ifp);
+ /* The AWDL interface has no vif, cfg80211 does not know it */
+ if (ifp && ifp->awdl) {
+ if (ifevent->action == BRCMF_E_IF_DEL &&
+ !brcmf_awdl_if_removed(drvr, ifp))
+ brcmf_remove_interface(ifp, false);
+ return;
+ }
+
brcmf_fweh_call_event_handler(drvr, ifp, emsg->event_code, emsg,
data);
@@ -313,6 +324,12 @@ static void brcmf_fweh_event_worker(struct work_struct *work)
ifp = drvr->iflist[0];
else
ifp = drvr->iflist[emsg.bsscfgidx];
+ /* The AWDL interface has no vif, its events are not for
+ * the cfg80211 handlers
+ */
+ if (ifp && ifp->awdl)
+ goto event_free;
+
err = brcmf_fweh_call_event_handler(drvr, ifp, event->code,
&emsg, event->data);
if (err) {
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 6/7] wifi: brcmfmac: add vendor commands for AWDL
2026-11-04 15:15 [PATCH 0/7] wifi: brcmfmac: AWDL for AirDrop on Apple T2 Macs Andre Eikmeyer
` (4 preceding siblings ...)
2026-11-04 15:15 ` [PATCH 5/7] wifi: brcmfmac: add the AWDL data path Andre Eikmeyer
@ 2026-11-04 15:15 ` Andre Eikmeyer
2026-11-04 15:15 ` [PATCH 7/7] wifi: brcmfmac: pass AWDL frames and role changes to user space Andre Eikmeyer
6 siblings, 0 replies; 8+ messages in thread
From: Andre Eikmeyer @ 2026-11-04 15:15 UTC (permalink / raw)
To: arend.vanspriel
Cc: linux-wireless, brcm80211, brcm80211-dev-list.pdl, linux-kernel,
Andre Eikmeyer, Yossef Osman, Aditya Garg
AWDL election, peer discovery and services run in user space. The
firmware sends and receives the AWDL frames and the data once the host
has set its AWDL parameters. Add vendor commands of the Broadcom OUI on
the primary interface for that:
- AWDL_START creates the AWDL interface with the given MAC address and
starts AWDL with the given TLVs. AWDL_STOP stops it and removes the
interface. Registering and unregistering the network device needs
RTNL, vendor commands run with the wiphy lock only. Both commands
therefore run in a work item and report their result with the vendor
events AWDL_STARTED and AWDL_STOPPED.
- AWDL_SET_FRAMES replaces the TLVs the firmware sends, the TLVs of
awdl_payload and awdl_oob_af_auto. The driver adds the headers.
- AWDL_SET_SYNC sets the master to synchronize to, with its metric and
the distance to it. Leaving out the master makes the device its own
master.
- AWDL_PEER_ADD and AWDL_PEER_DEL add and remove a peer data frames go
to. AWDL_PEER_ADD takes the HT and VHT capabilities elements of the
peer, which macOS builds from the AWDL TLVs of the peer and passes in
awdl_peer_op.
The other firmware parameters of AWDL are set by the driver in the
order and with the values macOS 15.4.1 sets on the BCM4377 of a
MacBookAir9,1. The BCM4364 gets slot_bss without SDB,
its firmware traps with the SDB form while the station is on 5 GHz.
Some parameters change 500 ms and 5 s after the start.
The AWDL interface is removed before system sleep with an AWDL_STOPPED
event.
Tested-by: Yossef Osman <Yossef.Osman@proton.me>
Tested-by: Aditya Garg <aditya.garg@linux.dev>
Signed-off-by: Andre Eikmeyer <dev@deq.rocks>
---
.../broadcom/brcm80211/brcmfmac/awdl.c | 586 +++++++++++++++++-
.../broadcom/brcm80211/brcmfmac/awdl.h | 53 ++
.../broadcom/brcm80211/brcmfmac/cfg80211.c | 5 +-
.../broadcom/brcm80211/brcmfmac/vendor.c | 78 +++
.../broadcom/brcm80211/brcmfmac/vendor.h | 86 +++
5 files changed, 804 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
index 95619283471..7df77a7fdf4 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
@@ -27,13 +27,18 @@
#include <linux/rtnetlink.h>
#include <linux/skbuff.h>
#include <linux/unaligned.h>
+#include <net/netlink.h>
+#include <brcm_hw_ids.h>
#include "core.h"
#include "debug.h"
#include "feature.h"
#include "fwil.h"
#include "fwil_types.h"
+#include "bus.h"
#include "proto.h"
+#include "cfg80211.h"
+#include "vendor.h"
#include "awdl.h"
/* The firmware interface index of the AWDL interface */
@@ -53,10 +58,123 @@ static const u8 brcmf_awdl_bssid[ETH_ALEN] = {
0x00, 0x25, 0x00, 0xff, 0x94, 0x73,
};
+/*
+ * Firmware parameters the host sets for AWDL. The values are the ones the
+ * firmware takes from the host it was made for. Their fields are not known
+ * beyond what the comments say.
+ */
+#define BRCMF_AWDL_CONFIG_START 0x83
+#define BRCMF_AWDL_CONFIG_SETTLED 0x73
+#define BRCMF_AWDL_PRESENCEMODE 4
+#define BRCMF_AWDL_OSOC_CHAN 44
+#define BRCMF_AWDL_AFTX_DELAY msecs_to_jiffies(500)
+#define BRCMF_AWDL_SETTLE_DELAY msecs_to_jiffies(5000)
+#define BRCMF_AWDL_TLV_MAX 2048
+
+static const u8 brcmf_awdl_sync_params[] = {
+ 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00,
+ 0x10, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x10, 0x00,
+ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+static const u8 brcmf_awdl_psf_dwell[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x6e, 0x00,
+};
+
+/* Header of awdl_oob_af_auto, the AWDL BSSID at 8, followed by the TLV length */
+static const u8 brcmf_awdl_oob_hdr[] = {
+ 0xff, 0xff, 0xff, 0xff, 0xd8, 0x40, 0x00, 0x00,
+ 0x00, 0x25, 0x00, 0xff, 0x94, 0x73, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+/* awdl_opmode: byte 1 the role, 2 master and 0 slave, 4 to 9 the master */
+#define BRCMF_AWDL_OPMODE_ROLE 1
+#define BRCMF_AWDL_OPMODE_ROLE_MASTER 2
+#define BRCMF_AWDL_OPMODE_ROLE_SLAVE 0
+#define BRCMF_AWDL_OPMODE_ADDR 4
+static const u8 brcmf_awdl_opmode[] = {
+ 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+/* awdl_election_tree: the master at 0x0f, its metric at 0x15, distance 0x19 */
+#define BRCMF_AWDL_ELECTION_ADDR 0x0f
+#define BRCMF_AWDL_ELECTION_METRIC 0x15
+#define BRCMF_AWDL_ELECTION_DISTANCE 0x19
+static const u8 brcmf_awdl_election_tree[] = {
+ 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x00,
+ 0x05, 0xba, 0x00, 0x00, 0x01, 0x03, 0x04, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+};
+
+/*
+ * slot_bss with the chanspecs of slice 0 at 0x4c and of slice 1 at 0x78, 16
+ * each. Chips without SDB get 9 at 0x0c and all chanspecs in slice 0.
+ */
+#define BRCMF_AWDL_SLOT_SDB 0x0c
+#define BRCMF_AWDL_SLOT_SDB_OFF 9
+#define BRCMF_AWDL_SLOT_SLICE0 0x4c
+#define BRCMF_AWDL_SLOT_SLICE1 0x78
+#define BRCMF_AWDL_SLOTS 16
+static const u8 brcmf_awdl_slot_bss[] = {
+ 0x00, 0x80, 0x01, 0x01, 0x01, 0x00, 0x90, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00,
+ 0x01, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0f, 0x02, 0x00, 0x03, 0xff, 0xff, 0xe2, 0x2a,
+ 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x06,
+ 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x02, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x10, 0x02, 0x00, 0xe2, 0x2a, 0xe2, 0x2a,
+ 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe2, 0x2a,
+ 0xe2, 0x2a, 0xe2, 0x2a, 0xe2, 0x2a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x10, 0x02, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+/*
+ * awdl_peer_op: byte 1 the operation, 2 to 7 the peer, 8 the flags, from 9
+ * the HT and VHT capabilities elements of the peer. An add without them is
+ * 0x25 bytes long.
+ */
+#define BRCMF_AWDL_PEER_OP_ADD 0
+#define BRCMF_AWDL_PEER_OP_DEL 1
+#define BRCMF_AWDL_PEER_OP_HDR_LEN 9
+#define BRCMF_AWDL_PEER_OP_ADD_LEN 0x25
+#define BRCMF_AWDL_PEER_IES_MAX 128
+
+static void brcmf_awdl_ctrl_work(struct work_struct *work);
+static void brcmf_awdl_aftx_work(struct wiphy *wiphy, struct wiphy_work *work);
+static void brcmf_awdl_settle_work(struct wiphy *wiphy,
+ struct wiphy_work *work);
+
void brcmf_awdl_attach(struct brcmf_pub *drvr)
{
- spin_lock_init(&drvr->awdl.lock);
- init_completion(&drvr->awdl.if_event);
+ struct brcmf_awdl *awdl = &drvr->awdl;
+
+ spin_lock_init(&awdl->lock);
+ init_completion(&awdl->if_event);
+ INIT_WORK(&awdl->ctrl_work, brcmf_awdl_ctrl_work);
+ wiphy_delayed_work_init(&awdl->aftx_work, brcmf_awdl_aftx_work);
+ wiphy_delayed_work_init(&awdl->settle_work, brcmf_awdl_settle_work);
}
/*
@@ -248,6 +366,8 @@ int brcmf_awdl_del(struct brcmf_pub *drvr)
if (!ifp)
return -ENOENT;
+ wiphy_delayed_work_cancel(drvr->wiphy, &awdl->aftx_work);
+ wiphy_delayed_work_cancel(drvr->wiphy, &awdl->settle_work);
netif_tx_disable(ifp->ndev);
/* Deletes the flowrings of all peers */
brcmf_proto_configure_addr_mode(drvr, ifp->ifidx, ADDR_INDIRECT);
@@ -335,6 +455,461 @@ int brcmf_awdl_decap(struct sk_buff *skb)
return 0;
}
+/* Sets a firmware parameter of the AWDL interface, errors are only logged */
+static void brcmf_awdl_set(struct brcmf_if *ifp, const char *name,
+ const void *data, u32 len)
+{
+ int err;
+
+ err = brcmf_fil_iovar_data_set(ifp, name, data, len);
+ if (err)
+ brcmf_dbg(INFO, "AWDL %s: %d\n", name, err);
+}
+
+static void brcmf_awdl_set_u32(struct brcmf_if *ifp, const char *name,
+ u32 val)
+{
+ __le32 data = cpu_to_le32(val);
+
+ brcmf_awdl_set(ifp, name, &data, sizeof(data));
+}
+
+static void brcmf_awdl_free_frames(struct brcmf_awdl *awdl)
+{
+ kfree(awdl->payload);
+ kfree(awdl->oob);
+ awdl->payload = NULL;
+ awdl->oob = NULL;
+ awdl->payload_len = 0;
+ awdl->oob_len = 0;
+}
+
+/* Builds awdl_payload and awdl_oob_af_auto around the TLVs of user space */
+static int brcmf_awdl_store_frames(struct brcmf_awdl *awdl,
+ const struct nlattr *payload,
+ const struct nlattr *oob)
+{
+ u32 plen = nla_len(payload), olen = nla_len(oob);
+ u8 *p, *o;
+
+ p = kmalloc(2 + plen, GFP_KERNEL);
+ o = kmalloc(sizeof(brcmf_awdl_oob_hdr) + 2 + olen, GFP_KERNEL);
+ if (!p || !o) {
+ kfree(p);
+ kfree(o);
+ return -ENOMEM;
+ }
+ put_unaligned_le16(plen, p);
+ memcpy(p + 2, nla_data(payload), plen);
+ memcpy(o, brcmf_awdl_oob_hdr, sizeof(brcmf_awdl_oob_hdr));
+ put_unaligned_le16(olen, o + sizeof(brcmf_awdl_oob_hdr));
+ memcpy(o + sizeof(brcmf_awdl_oob_hdr) + 2, nla_data(oob), olen);
+
+ brcmf_awdl_free_frames(awdl);
+ awdl->payload = p;
+ awdl->payload_len = 2 + plen;
+ awdl->oob = o;
+ awdl->oob_len = sizeof(brcmf_awdl_oob_hdr) + 2 + olen;
+ return 0;
+}
+
+static void brcmf_awdl_set_payload(struct brcmf_awdl *awdl)
+{
+ brcmf_awdl_set(awdl->ifp, "awdl_payload", awdl->payload,
+ awdl->payload_len);
+}
+
+static void brcmf_awdl_set_opmode(struct brcmf_awdl *awdl)
+{
+ u8 buf[sizeof(brcmf_awdl_opmode)];
+
+ memcpy(buf, brcmf_awdl_opmode, sizeof(buf));
+ if (awdl->has_master) {
+ buf[BRCMF_AWDL_OPMODE_ROLE] = BRCMF_AWDL_OPMODE_ROLE_SLAVE;
+ ether_addr_copy(buf + BRCMF_AWDL_OPMODE_ADDR, awdl->master);
+ } else {
+ buf[BRCMF_AWDL_OPMODE_ROLE] = BRCMF_AWDL_OPMODE_ROLE_MASTER;
+ ether_addr_copy(buf + BRCMF_AWDL_OPMODE_ADDR, awdl->mac);
+ }
+ brcmf_awdl_set(awdl->ifp, "awdl_opmode", buf, sizeof(buf));
+}
+
+static void brcmf_awdl_set_election(struct brcmf_awdl *awdl)
+{
+ u8 buf[sizeof(brcmf_awdl_election_tree)];
+
+ memcpy(buf, brcmf_awdl_election_tree, sizeof(buf));
+ if (awdl->has_master) {
+ ether_addr_copy(buf + BRCMF_AWDL_ELECTION_ADDR, awdl->master);
+ put_unaligned_le32(awdl->master_metric,
+ buf + BRCMF_AWDL_ELECTION_METRIC);
+ buf[BRCMF_AWDL_ELECTION_DISTANCE] = awdl->master_distance;
+ } else {
+ ether_addr_copy(buf + BRCMF_AWDL_ELECTION_ADDR, awdl->mac);
+ }
+ brcmf_awdl_set(awdl->ifp, "awdl_election_tree", buf, sizeof(buf));
+}
+
+static void brcmf_awdl_set_slots(struct brcmf_pub *drvr)
+{
+ u8 buf[sizeof(brcmf_awdl_slot_bss)];
+ u8 *s0, *s1;
+ int i;
+
+ memcpy(buf, brcmf_awdl_slot_bss, sizeof(buf));
+ /* The BCM4364 firmware traps with the SDB form while the station is
+ * on 5 GHz
+ */
+ if (drvr->bus_if->chip == BRCM_CC_4364_CHIP_ID) {
+ buf[BRCMF_AWDL_SLOT_SDB] = BRCMF_AWDL_SLOT_SDB_OFF;
+ for (i = 0; i < BRCMF_AWDL_SLOTS; i++) {
+ s0 = buf + BRCMF_AWDL_SLOT_SLICE0 + 2 * i;
+ s1 = buf + BRCMF_AWDL_SLOT_SLICE1 + 2 * i;
+ if (!get_unaligned_le16(s0))
+ memcpy(s0, s1, 2);
+ memset(s1, 0, 2);
+ }
+ }
+ brcmf_awdl_set(drvr->awdl.ifp, "slot_bss", buf, sizeof(buf));
+}
+
+/* Sends the frames and the synchronization state again */
+static void brcmf_awdl_refresh(struct brcmf_awdl *awdl)
+{
+ brcmf_awdl_set(awdl->ifp, "awdl_oob_af_auto", awdl->oob,
+ awdl->oob_len);
+ brcmf_awdl_set_payload(awdl);
+ brcmf_awdl_set(awdl->ifp, "awdl_sync_params", brcmf_awdl_sync_params,
+ sizeof(brcmf_awdl_sync_params));
+ brcmf_awdl_set_opmode(awdl);
+ brcmf_awdl_set_election(awdl);
+ brcmf_awdl_set(awdl->ifp, "awdl_sync_params", brcmf_awdl_sync_params,
+ sizeof(brcmf_awdl_sync_params));
+}
+
+/* Starts AWDL on the AWDL interface, the firmware expects this order */
+static void brcmf_awdl_enable(struct brcmf_pub *drvr)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ struct brcmf_if *ifp = awdl->ifp;
+
+ brcmf_awdl_set_payload(awdl);
+ brcmf_awdl_set_u32(ifp, "awdl_aftxmode", 0);
+ brcmf_awdl_set(ifp, "awdl_sync_params", brcmf_awdl_sync_params,
+ sizeof(brcmf_awdl_sync_params));
+ brcmf_awdl_set_u32(ifp, "awdl_osoc_chan", 0);
+ brcmf_awdl_set_u32(ifp, "awdl_config", BRCMF_AWDL_CONFIG_START);
+ brcmf_awdl_set(ifp, "awdl_psf_dwell", brcmf_awdl_psf_dwell,
+ sizeof(brcmf_awdl_psf_dwell));
+ brcmf_awdl_set_slots(drvr);
+ brcmf_awdl_set_payload(awdl);
+ brcmf_awdl_set_election(awdl);
+ brcmf_awdl_set_u32(ifp, "awdl", 1);
+ brcmf_awdl_set_u32(ifp, "awdl_presencemode", BRCMF_AWDL_PRESENCEMODE);
+ brcmf_awdl_set_slots(drvr);
+ brcmf_awdl_refresh(awdl);
+ wiphy_delayed_work_queue(drvr->wiphy, &awdl->aftx_work,
+ BRCMF_AWDL_AFTX_DELAY);
+ wiphy_delayed_work_queue(drvr->wiphy, &awdl->settle_work,
+ BRCMF_AWDL_SETTLE_DELAY);
+}
+
+static void brcmf_awdl_aftx_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+ struct brcmf_awdl *awdl = container_of(work, struct brcmf_awdl,
+ aftx_work.work);
+
+ if (!awdl->ifp)
+ return;
+ brcmf_awdl_set_u32(awdl->ifp, "awdl_aftxmode", 0);
+ brcmf_awdl_set_u32(awdl->ifp, "awdl_osoc_chan", BRCMF_AWDL_OSOC_CHAN);
+}
+
+static void brcmf_awdl_settle_work(struct wiphy *wiphy,
+ struct wiphy_work *work)
+{
+ struct brcmf_awdl *awdl = container_of(work, struct brcmf_awdl,
+ settle_work.work);
+ static const u8 dwell_off[sizeof(brcmf_awdl_psf_dwell)];
+
+ if (!awdl->ifp)
+ return;
+ brcmf_awdl_set_u32(awdl->ifp, "awdl_config", BRCMF_AWDL_CONFIG_SETTLED);
+ brcmf_awdl_set(awdl->ifp, "awdl_psf_dwell", dwell_off,
+ sizeof(dwell_off));
+}
+
+static void brcmf_awdl_send_result(struct brcmf_pub *drvr, int event,
+ int status, struct net_device *ndev)
+{
+ struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
+ struct sk_buff *skb;
+
+ skb = cfg80211_vendor_event_alloc(drvr->wiphy,
+ pri && pri->vif ? &pri->vif->wdev :
+ NULL,
+ NLMSG_DEFAULT_SIZE, event,
+ GFP_KERNEL);
+ if (!skb)
+ return;
+ if (nla_put_s32(skb, BRCMF_AWDL_ATTR_STATUS, status) ||
+ (ndev && nla_put_u32(skb, BRCMF_AWDL_ATTR_IFINDEX,
+ ndev->ifindex))) {
+ kfree_skb(skb);
+ return;
+ }
+ cfg80211_vendor_event(skb, GFP_KERNEL);
+}
+
+/* Runs the AWDL_START and AWDL_STOP vendor commands with RTNL */
+static void brcmf_awdl_ctrl_work(struct work_struct *work)
+{
+ struct brcmf_awdl *awdl = container_of(work, struct brcmf_awdl,
+ ctrl_work);
+ struct brcmf_pub *drvr = container_of(awdl, struct brcmf_pub, awdl);
+ struct net_device *ndev = NULL;
+ enum brcmf_awdl_op op;
+ int err;
+
+ rtnl_lock();
+ wiphy_lock(drvr->wiphy);
+ spin_lock(&awdl->lock);
+ op = awdl->op;
+ awdl->op = BRCMF_AWDL_OP_NONE;
+ spin_unlock(&awdl->lock);
+
+ switch (op) {
+ case BRCMF_AWDL_OP_START:
+ err = brcmf_awdl_add(drvr, awdl->start_mac, &ndev);
+ if (!err) {
+ awdl->has_master = false;
+ brcmf_awdl_enable(drvr);
+ }
+ brcmf_awdl_send_result(drvr, BRCMF_VNDR_EVTS_AWDL_STARTED, err,
+ ndev);
+ break;
+ case BRCMF_AWDL_OP_STOP:
+ err = brcmf_awdl_del(drvr);
+ brcmf_awdl_send_result(drvr, BRCMF_VNDR_EVTS_AWDL_STOPPED, err,
+ NULL);
+ break;
+ default:
+ break;
+ }
+ wiphy_unlock(drvr->wiphy);
+ rtnl_unlock();
+}
+
+/*
+ * Removes the AWDL interface before system sleep, with RTNL and the wiphy
+ * mutex held, while the firmware still takes commands. A queued operation is
+ * dropped.
+ */
+void brcmf_awdl_suspend(struct brcmf_pub *drvr)
+{
+ struct brcmf_awdl *awdl = &drvr->awdl;
+ int err;
+
+ spin_lock(&awdl->lock);
+ awdl->op = BRCMF_AWDL_OP_NONE;
+ spin_unlock(&awdl->lock);
+ if (!awdl->ifp)
+ return;
+ err = brcmf_awdl_del(drvr);
+ brcmf_awdl_send_result(drvr, BRCMF_VNDR_EVTS_AWDL_STOPPED, err, NULL);
+}
+
+const struct nla_policy brcmf_awdl_vndr_policy[BRCMF_AWDL_ATTR_MAX + 1] = {
+ [BRCMF_AWDL_ATTR_ADDR] = NLA_POLICY_ETH_ADDR,
+ [BRCMF_AWDL_ATTR_PAYLOAD] = { .type = NLA_BINARY,
+ .len = BRCMF_AWDL_TLV_MAX },
+ [BRCMF_AWDL_ATTR_OOB] = { .type = NLA_BINARY,
+ .len = BRCMF_AWDL_TLV_MAX },
+ [BRCMF_AWDL_ATTR_MASTER] = NLA_POLICY_ETH_ADDR,
+ [BRCMF_AWDL_ATTR_MASTER_METRIC] = { .type = NLA_U32 },
+ [BRCMF_AWDL_ATTR_MASTER_DISTANCE] = { .type = NLA_U8 },
+ [BRCMF_AWDL_ATTR_STATUS] = { .type = NLA_REJECT },
+ [BRCMF_AWDL_ATTR_IFINDEX] = { .type = NLA_REJECT },
+ [BRCMF_AWDL_ATTR_PEER_IES] = { .type = NLA_BINARY,
+ .len = BRCMF_AWDL_PEER_IES_MAX },
+};
+
+/* The device of a vendor command and its parsed attributes */
+static struct brcmf_pub *brcmf_awdl_vndr_parse(struct wiphy *wiphy,
+ const void *data, int len,
+ struct nlattr **tb)
+{
+ struct brcmf_cfg80211_info *cfg = wiphy_to_cfg(wiphy);
+ struct brcmf_pub *drvr = cfg->pub;
+ struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
+ int err;
+
+ if (!pri || !brcmf_feat_is_enabled(pri, BRCMF_FEAT_AWDL))
+ return ERR_PTR(-EOPNOTSUPP);
+ err = nla_parse(tb, BRCMF_AWDL_ATTR_MAX, data, len,
+ brcmf_awdl_vndr_policy, NULL);
+ if (err)
+ return ERR_PTR(err);
+ return drvr;
+}
+
+int brcmf_awdl_vndr_start(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ struct nlattr *tb[BRCMF_AWDL_ATTR_MAX + 1];
+ struct brcmf_pub *drvr;
+ struct brcmf_awdl *awdl;
+ int err;
+
+ drvr = brcmf_awdl_vndr_parse(wiphy, data, len, tb);
+ if (IS_ERR(drvr))
+ return PTR_ERR(drvr);
+ awdl = &drvr->awdl;
+ if (!tb[BRCMF_AWDL_ATTR_ADDR] || !tb[BRCMF_AWDL_ATTR_PAYLOAD] ||
+ !tb[BRCMF_AWDL_ATTR_OOB])
+ return -EINVAL;
+ if (!is_valid_ether_addr(nla_data(tb[BRCMF_AWDL_ATTR_ADDR])))
+ return -EINVAL;
+ if (awdl->detached)
+ return -ENODEV;
+ if (awdl->ifp || awdl->op != BRCMF_AWDL_OP_NONE)
+ return -EBUSY;
+
+ err = brcmf_awdl_store_frames(awdl, tb[BRCMF_AWDL_ATTR_PAYLOAD],
+ tb[BRCMF_AWDL_ATTR_OOB]);
+ if (err)
+ return err;
+ ether_addr_copy(awdl->start_mac, nla_data(tb[BRCMF_AWDL_ATTR_ADDR]));
+ spin_lock(&awdl->lock);
+ awdl->op = BRCMF_AWDL_OP_START;
+ spin_unlock(&awdl->lock);
+ schedule_work(&awdl->ctrl_work);
+ return 0;
+}
+
+int brcmf_awdl_vndr_stop(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ struct nlattr *tb[BRCMF_AWDL_ATTR_MAX + 1];
+ struct brcmf_pub *drvr;
+ struct brcmf_awdl *awdl;
+
+ drvr = brcmf_awdl_vndr_parse(wiphy, data, len, tb);
+ if (IS_ERR(drvr))
+ return PTR_ERR(drvr);
+ awdl = &drvr->awdl;
+ if (awdl->detached)
+ return -ENODEV;
+ if (!awdl->ifp && awdl->op != BRCMF_AWDL_OP_START)
+ return -ENOENT;
+
+ spin_lock(&awdl->lock);
+ awdl->op = BRCMF_AWDL_OP_STOP;
+ spin_unlock(&awdl->lock);
+ schedule_work(&awdl->ctrl_work);
+ return 0;
+}
+
+int brcmf_awdl_vndr_set_frames(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ struct nlattr *tb[BRCMF_AWDL_ATTR_MAX + 1];
+ struct brcmf_pub *drvr;
+ int err;
+
+ drvr = brcmf_awdl_vndr_parse(wiphy, data, len, tb);
+ if (IS_ERR(drvr))
+ return PTR_ERR(drvr);
+ if (!tb[BRCMF_AWDL_ATTR_PAYLOAD] || !tb[BRCMF_AWDL_ATTR_OOB])
+ return -EINVAL;
+ if (!drvr->awdl.ifp)
+ return -ENOENT;
+
+ err = brcmf_awdl_store_frames(&drvr->awdl, tb[BRCMF_AWDL_ATTR_PAYLOAD],
+ tb[BRCMF_AWDL_ATTR_OOB]);
+ if (err)
+ return err;
+ brcmf_awdl_refresh(&drvr->awdl);
+ return 0;
+}
+
+int brcmf_awdl_vndr_set_sync(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ struct nlattr *tb[BRCMF_AWDL_ATTR_MAX + 1];
+ struct brcmf_pub *drvr;
+ struct brcmf_awdl *awdl;
+
+ drvr = brcmf_awdl_vndr_parse(wiphy, data, len, tb);
+ if (IS_ERR(drvr))
+ return PTR_ERR(drvr);
+ awdl = &drvr->awdl;
+ if (!awdl->ifp)
+ return -ENOENT;
+
+ if (tb[BRCMF_AWDL_ATTR_MASTER]) {
+ if (!tb[BRCMF_AWDL_ATTR_MASTER_METRIC] ||
+ !tb[BRCMF_AWDL_ATTR_MASTER_DISTANCE])
+ return -EINVAL;
+ awdl->has_master = true;
+ ether_addr_copy(awdl->master,
+ nla_data(tb[BRCMF_AWDL_ATTR_MASTER]));
+ awdl->master_metric =
+ nla_get_u32(tb[BRCMF_AWDL_ATTR_MASTER_METRIC]);
+ awdl->master_distance =
+ nla_get_u8(tb[BRCMF_AWDL_ATTR_MASTER_DISTANCE]);
+ } else {
+ awdl->has_master = false;
+ }
+ brcmf_awdl_refresh(awdl);
+ return 0;
+}
+
+static int brcmf_awdl_peer_op(struct wiphy *wiphy, const void *data, int len,
+ bool add)
+{
+ u8 op[BRCMF_AWDL_PEER_OP_HDR_LEN + BRCMF_AWDL_PEER_IES_MAX] = {};
+ struct nlattr *tb[BRCMF_AWDL_ATTR_MAX + 1];
+ u32 op_len = BRCMF_AWDL_PEER_OP_HDR_LEN;
+ struct brcmf_pub *drvr;
+ struct nlattr *ies;
+
+ drvr = brcmf_awdl_vndr_parse(wiphy, data, len, tb);
+ if (IS_ERR(drvr))
+ return PTR_ERR(drvr);
+ if (!tb[BRCMF_AWDL_ATTR_ADDR])
+ return -EINVAL;
+ if (!drvr->awdl.ifp)
+ return -ENOENT;
+
+ op[1] = add ? BRCMF_AWDL_PEER_OP_ADD : BRCMF_AWDL_PEER_OP_DEL;
+ ether_addr_copy(op + 2, nla_data(tb[BRCMF_AWDL_ATTR_ADDR]));
+ op[8] = add;
+ if (add) {
+ ies = tb[BRCMF_AWDL_ATTR_PEER_IES];
+ if (ies) {
+ memcpy(op + BRCMF_AWDL_PEER_OP_HDR_LEN, nla_data(ies),
+ nla_len(ies));
+ op_len += nla_len(ies);
+ }
+ op_len = max_t(u32, op_len, BRCMF_AWDL_PEER_OP_ADD_LEN);
+ }
+ return brcmf_fil_iovar_data_set(drvr->awdl.ifp, "awdl_peer_op", op,
+ op_len);
+}
+
+int brcmf_awdl_vndr_peer_add(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ return brcmf_awdl_peer_op(wiphy, data, len, true);
+}
+
+int brcmf_awdl_vndr_peer_del(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len)
+{
+ return brcmf_awdl_peer_op(wiphy, data, len, false);
+}
+
/* Called before the interfaces of the device are removed */
void brcmf_awdl_detach(struct brcmf_pub *drvr)
{
@@ -342,10 +917,17 @@ void brcmf_awdl_detach(struct brcmf_pub *drvr)
rtnl_lock();
wiphy_lock(drvr->wiphy);
+ wiphy_delayed_work_cancel(drvr->wiphy, &awdl->aftx_work);
+ wiphy_delayed_work_cancel(drvr->wiphy, &awdl->settle_work);
spin_lock(&awdl->lock);
awdl->detached = true;
+ awdl->op = BRCMF_AWDL_OP_NONE;
awdl->ifp = NULL;
spin_unlock(&awdl->lock);
+ brcmf_awdl_free_frames(awdl);
wiphy_unlock(drvr->wiphy);
rtnl_unlock();
+
+ /* A queued operation finds the device detached */
+ cancel_work_sync(&awdl->ctrl_work);
}
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
index 3edc0e96bb9..63030221dea 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
@@ -10,12 +10,21 @@
#include <linux/completion.h>
#include <linux/if_ether.h>
#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+#include <net/cfg80211.h>
+#include <net/netlink.h>
struct brcmf_pub;
struct brcmf_if;
struct net_device;
struct sk_buff;
+enum brcmf_awdl_op {
+ BRCMF_AWDL_OP_NONE,
+ BRCMF_AWDL_OP_START,
+ BRCMF_AWDL_OP_STOP,
+};
+
/**
* struct brcmf_awdl - AWDL interface of a device.
*
@@ -28,6 +37,22 @@ struct sk_buff;
* @detached: the device goes away, no new AWDL interface is created.
* @ifp: the AWDL interface, NULL when there is none.
* @tx_seq: sequence number of the next data frame sent.
+ * @ctrl_work: creates or removes the AWDL interface for the vendor commands,
+ * which run without RTNL.
+ * @op: operation of @ctrl_work, under @lock.
+ * @start_mac: MAC address for %BRCMF_AWDL_OP_START.
+ * @payload: awdl_payload with the TLVs of user space.
+ * @payload_len: length of @payload.
+ * @oob: awdl_oob_af_auto with the TLVs of user space.
+ * @oob_len: length of @oob.
+ * @has_master: another device is the master.
+ * @master: MAC address of the master.
+ * @master_metric: metric of the master.
+ * @master_distance: hops to the master.
+ * @aftx_work: changes the firmware parameters 500 ms after the start.
+ * @settle_work: changes the firmware parameters 5 s after the start.
+ *
+ * The members from @start_mac on are protected by the wiphy mutex.
*/
struct brcmf_awdl {
spinlock_t lock; /* AWDL state against the event worker */
@@ -38,6 +63,19 @@ struct brcmf_awdl {
bool detached;
struct brcmf_if *ifp;
u16 tx_seq;
+ struct work_struct ctrl_work;
+ enum brcmf_awdl_op op;
+ u8 start_mac[ETH_ALEN];
+ u8 *payload;
+ u32 payload_len;
+ u8 *oob;
+ u32 oob_len;
+ bool has_master;
+ u8 master[ETH_ALEN];
+ u32 master_metric;
+ u8 master_distance;
+ struct wiphy_delayed_work aftx_work;
+ struct wiphy_delayed_work settle_work;
};
/* Length of the LLC/SNAP and AWDL data header of AWDL data frames */
@@ -48,9 +86,24 @@ void brcmf_awdl_detach(struct brcmf_pub *drvr);
int brcmf_awdl_add(struct brcmf_pub *drvr, const u8 *mac,
struct net_device **ndev);
int brcmf_awdl_del(struct brcmf_pub *drvr);
+void brcmf_awdl_suspend(struct brcmf_pub *drvr);
bool brcmf_awdl_if_added(struct brcmf_pub *drvr, struct brcmf_if *ifp);
bool brcmf_awdl_if_removed(struct brcmf_pub *drvr, struct brcmf_if *ifp);
int brcmf_awdl_encap(struct brcmf_if *ifp, struct sk_buff *skb);
int brcmf_awdl_decap(struct sk_buff *skb);
+extern const struct nla_policy brcmf_awdl_vndr_policy[];
+int brcmf_awdl_vndr_start(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+int brcmf_awdl_vndr_stop(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+int brcmf_awdl_vndr_set_frames(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+int brcmf_awdl_vndr_set_sync(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+int brcmf_awdl_vndr_peer_add(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+int brcmf_awdl_vndr_peer_del(struct wiphy *wiphy, struct wireless_dev *wdev,
+ const void *data, int len);
+
#endif /* BRCMFMAC_AWDL_H */
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
index 4f35dbef082..41ea0481cf1 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
@@ -4282,8 +4282,7 @@ static s32 brcmf_cfg80211_suspend(struct wiphy *wiphy,
/* The AWDL interface does not survive sleep, the firmware still
* takes commands here. User space creates it again after resume.
*/
- if (cfg->pub->awdl.ifp)
- brcmf_awdl_del(cfg->pub);
+ brcmf_awdl_suspend(cfg->pub);
/* if the primary net_device is not READY there is nothing
* we can do but pray resume goes smoothly.
@@ -7901,6 +7900,8 @@ static int brcmf_setup_wiphy(struct wiphy *wiphy, struct brcmf_if *ifp)
/* vendor commands/events support */
wiphy->vendor_commands = brcmf_vendor_cmds;
wiphy->n_vendor_commands = BRCMF_VNDR_CMDS_LAST - 1;
+ wiphy->vendor_events = brcmf_vendor_events;
+ wiphy->n_vendor_events = BRCMF_VNDR_EVTS_LAST;
if (brcmf_feat_is_enabled(ifp, BRCMF_FEAT_WOWL))
brcmf_wiphy_wowl_params(wiphy, ifp);
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
index d07e7c7355d..5e7de07c995 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
@@ -15,6 +15,7 @@
#include "cfg80211.h"
#include "vendor.h"
#include "fwil.h"
+#include "awdl.h"
static int brcmf_cfg80211_vndr_cmds_dcmd_handler(struct wiphy *wiphy,
struct wireless_dev *wdev,
@@ -115,4 +116,81 @@ const struct wiphy_vendor_command brcmf_vendor_cmds[] = {
.policy = VENDOR_CMD_RAW_DATA,
.doit = brcmf_cfg80211_vndr_cmds_dcmd_handler
},
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_START
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_start
+ },
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_STOP
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_stop
+ },
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_SET_FRAMES
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_set_frames
+ },
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_SET_SYNC
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_set_sync
+ },
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_PEER_ADD
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_peer_add
+ },
+ {
+ {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_CMDS_AWDL_PEER_DEL
+ },
+ .flags = WIPHY_VENDOR_CMD_NEED_WDEV |
+ WIPHY_VENDOR_CMD_NEED_NETDEV,
+ .policy = brcmf_awdl_vndr_policy,
+ .maxattr = BRCMF_AWDL_ATTR_MAX,
+ .doit = brcmf_awdl_vndr_peer_del
+ },
+};
+
+const struct nl80211_vendor_cmd_info brcmf_vendor_events[] = {
+ [BRCMF_VNDR_EVTS_AWDL_STARTED] = {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_EVTS_AWDL_STARTED
+ },
+ [BRCMF_VNDR_EVTS_AWDL_STOPPED] = {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_EVTS_AWDL_STOPPED
+ },
};
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
index 418f33ea6fd..ce7f7c46710 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
@@ -8,12 +8,97 @@
#define BROADCOM_OUI 0x001018
+/**
+ * enum brcmf_vndr_cmds - vendor commands of the Broadcom OUI
+ *
+ * @BRCMF_VNDR_CMDS_DCMD: firmware command, see struct brcmf_vndr_dcmd_hdr.
+ * @BRCMF_VNDR_CMDS_AWDL_START: create the AWDL interface with
+ * %BRCMF_AWDL_ATTR_ADDR and start AWDL with the TLVs of
+ * %BRCMF_AWDL_ATTR_PAYLOAD and %BRCMF_AWDL_ATTR_OOB. Runs asynchronously,
+ * the result comes with %BRCMF_VNDR_EVTS_AWDL_STARTED.
+ * @BRCMF_VNDR_CMDS_AWDL_STOP: stop AWDL and remove the AWDL interface. Runs
+ * asynchronously, the result comes with %BRCMF_VNDR_EVTS_AWDL_STOPPED.
+ * @BRCMF_VNDR_CMDS_AWDL_SET_FRAMES: replace the TLVs of
+ * %BRCMF_AWDL_ATTR_PAYLOAD and %BRCMF_AWDL_ATTR_OOB.
+ * @BRCMF_VNDR_CMDS_AWDL_SET_SYNC: synchronize to the master of
+ * %BRCMF_AWDL_ATTR_MASTER, %BRCMF_AWDL_ATTR_MASTER_METRIC and
+ * %BRCMF_AWDL_ATTR_MASTER_DISTANCE. Without %BRCMF_AWDL_ATTR_MASTER the
+ * device is its own master.
+ * @BRCMF_VNDR_CMDS_AWDL_PEER_ADD: add the peer %BRCMF_AWDL_ATTR_ADDR, data
+ * frames to it are sent from then on. %BRCMF_AWDL_ATTR_PEER_IES gives its
+ * HT and VHT capabilities, sent again it updates them.
+ * @BRCMF_VNDR_CMDS_AWDL_PEER_DEL: remove the peer %BRCMF_AWDL_ATTR_ADDR.
+ * @BRCMF_VNDR_CMDS_LAST: number of vendor commands plus one.
+ *
+ * The AWDL commands are sent to the primary interface. Election, peer
+ * discovery and services are left to user space, the firmware sends and
+ * receives the AWDL frames and data.
+ */
enum brcmf_vndr_cmds {
BRCMF_VNDR_CMDS_UNSPEC,
BRCMF_VNDR_CMDS_DCMD,
+ BRCMF_VNDR_CMDS_AWDL_START,
+ BRCMF_VNDR_CMDS_AWDL_STOP,
+ BRCMF_VNDR_CMDS_AWDL_SET_FRAMES,
+ BRCMF_VNDR_CMDS_AWDL_SET_SYNC,
+ BRCMF_VNDR_CMDS_AWDL_PEER_ADD,
+ BRCMF_VNDR_CMDS_AWDL_PEER_DEL,
BRCMF_VNDR_CMDS_LAST
};
+/**
+ * enum brcmf_vndr_evts - vendor events of the Broadcom OUI
+ *
+ * @BRCMF_VNDR_EVTS_AWDL_STARTED: result of %BRCMF_VNDR_CMDS_AWDL_START with
+ * %BRCMF_AWDL_ATTR_STATUS and, on success, %BRCMF_AWDL_ATTR_IFINDEX.
+ * @BRCMF_VNDR_EVTS_AWDL_STOPPED: the AWDL interface was removed, after
+ * %BRCMF_VNDR_CMDS_AWDL_STOP or before system sleep, with
+ * %BRCMF_AWDL_ATTR_STATUS.
+ * @BRCMF_VNDR_EVTS_LAST: number of vendor events.
+ *
+ * The value of an event is its index in the vendor events and its vendor
+ * subcommand.
+ */
+enum brcmf_vndr_evts {
+ BRCMF_VNDR_EVTS_AWDL_STARTED,
+ BRCMF_VNDR_EVTS_AWDL_STOPPED,
+ BRCMF_VNDR_EVTS_LAST
+};
+
+/**
+ * enum brcmf_awdl_attrs - attributes of the AWDL vendor commands and events
+ *
+ * @BRCMF_AWDL_ATTR_ADDR: MAC address, of the AWDL interface or of a peer.
+ * @BRCMF_AWDL_ATTR_PAYLOAD: AWDL TLVs for the firmware parameter
+ * awdl_payload.
+ * @BRCMF_AWDL_ATTR_OOB: AWDL TLVs for the firmware parameter
+ * awdl_oob_af_auto.
+ * @BRCMF_AWDL_ATTR_MASTER: MAC address of the master (u8[6]).
+ * @BRCMF_AWDL_ATTR_MASTER_METRIC: metric of the master (u32).
+ * @BRCMF_AWDL_ATTR_MASTER_DISTANCE: hops from this device to the master (u8).
+ * @BRCMF_AWDL_ATTR_STATUS: 0 or a negative error code (s32).
+ * @BRCMF_AWDL_ATTR_IFINDEX: interface index of the AWDL interface (u32).
+ * @BRCMF_AWDL_ATTR_PEER_IES: HT and VHT capabilities elements of a peer,
+ * built from the AWDL TLVs it sends, as macOS passes them.
+ * @BRCMF_AWDL_ATTR_MAX: highest attribute.
+ */
+enum brcmf_awdl_attrs {
+ BRCMF_AWDL_ATTR_UNSPEC,
+
+ BRCMF_AWDL_ATTR_ADDR,
+ BRCMF_AWDL_ATTR_PAYLOAD,
+ BRCMF_AWDL_ATTR_OOB,
+ BRCMF_AWDL_ATTR_MASTER,
+ BRCMF_AWDL_ATTR_MASTER_METRIC,
+ BRCMF_AWDL_ATTR_MASTER_DISTANCE,
+ BRCMF_AWDL_ATTR_STATUS,
+ BRCMF_AWDL_ATTR_IFINDEX,
+ BRCMF_AWDL_ATTR_PEER_IES,
+
+ __BRCMF_AWDL_ATTR_AFTER_LAST,
+ BRCMF_AWDL_ATTR_MAX = __BRCMF_AWDL_ATTR_AFTER_LAST - 1
+};
+
/**
* enum brcmf_nlattrs - nl80211 message attributes
*
@@ -49,5 +134,6 @@ struct brcmf_vndr_dcmd_hdr {
};
extern const struct wiphy_vendor_command brcmf_vendor_cmds[];
+extern const struct nl80211_vendor_cmd_info brcmf_vendor_events[];
#endif /* _vendor_h_ */
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 7/7] wifi: brcmfmac: pass AWDL frames and role changes to user space
2026-11-04 15:15 [PATCH 0/7] wifi: brcmfmac: AWDL for AirDrop on Apple T2 Macs Andre Eikmeyer
` (5 preceding siblings ...)
2026-11-04 15:15 ` [PATCH 6/7] wifi: brcmfmac: add vendor commands for AWDL Andre Eikmeyer
@ 2026-11-04 15:15 ` Andre Eikmeyer
6 siblings, 0 replies; 8+ messages in thread
From: Andre Eikmeyer @ 2026-11-04 15:15 UTC (permalink / raw)
To: arend.vanspriel
Cc: linux-wireless, brcm80211, brcm80211-dev-list.pdl, linux-kernel,
Andre Eikmeyer, Yossef Osman, Aditya Garg
User space runs the AWDL election and peer discovery from the AWDL
action frames other devices send. The firmware reports them with two
events on the AWDL interface, ACTION_FRAME_RX and AWDL_EVENT (98). A
header is in front of the frame. Its first 16 bits, high byte first,
are its version: 1 is struct brcmf_rx_mgmt_data with 16 bytes, 2 has 24
bytes. Both events come with either version. AWDL_EVENT carries a frame
only with some of its status values. AWDL_ROLE (97) reports the AWDL
role of the firmware in its status, 0 slave, 1 master not taking part
in the election and 2 master.
Register the two AWDL events when the firmware supports AWDL, so the
event mask includes them. Pass every AWDL action frame (vendor specific
category, OUI 00:17:f2, type 8) of the two events and the role changes
to user space as the vendor events AWDL_FRAME and AWDL_ROLE. Other
events of the AWDL interface are dropped as before.
Tested-by: Yossef Osman <Yossef.Osman@proton.me>
Tested-by: Aditya Garg <aditya.garg@linux.dev>
Signed-off-by: Andre Eikmeyer <dev@deq.rocks>
---
.../broadcom/brcm80211/brcmfmac/awdl.c | 122 +++++++++++++++++-
.../broadcom/brcm80211/brcmfmac/awdl.h | 4 +
.../broadcom/brcm80211/brcmfmac/cfg80211.c | 1 +
.../broadcom/brcm80211/brcmfmac/fweh.c | 8 +-
.../broadcom/brcm80211/brcmfmac/fweh.h | 5 +
.../broadcom/brcm80211/brcmfmac/vendor.c | 8 ++
.../broadcom/brcm80211/brcmfmac/vendor.h | 12 ++
7 files changed, 151 insertions(+), 9 deletions(-)
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
index 7df77a7fdf4..c8d92e84c5a 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.c
@@ -639,17 +639,125 @@ static void brcmf_awdl_settle_work(struct wiphy *wiphy,
sizeof(dwell_off));
}
+/*
+ * ACTION_FRAME_RX and AWDL_EVENT put a header in front of a received frame.
+ * Its first 16 bits, high byte first, are the version: 1 is struct
+ * brcmf_rx_mgmt_data with 16 bytes, 2 has 24 bytes.
+ */
+#define BRCMF_AWDL_RX_HDR_V1_LEN 16
+#define BRCMF_AWDL_RX_HDR_V2_LEN 24
+
+/* AWDL action frame: vendor specific category, Apple OUI, type AWDL */
+static const u8 brcmf_awdl_af[] = { 0x7f, 0x00, 0x17, 0xf2, 0x08 };
+
+static struct sk_buff *brcmf_awdl_event_alloc(struct brcmf_pub *drvr,
+ int event, int len)
+{
+ struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
+
+ return cfg80211_vendor_event_alloc(drvr->wiphy,
+ pri && pri->vif ? &pri->vif->wdev :
+ NULL,
+ len, event, GFP_KERNEL);
+}
+
+/**
+ * brcmf_awdl_fweh_event() - pass a firmware event of the AWDL interface on.
+ *
+ * @ifp: interface of the event.
+ * @e: event message.
+ * @data: event data.
+ *
+ * Called from the firmware event worker for the events of the AWDL
+ * interface, which do not go to the cfg80211 handlers. Received AWDL action
+ * frames and role changes become vendor events, other events are dropped.
+ * AWDL_EVENT carries an AWDL action frame with some of its status values.
+ * Return: 0 or a negative error code.
+ */
+int brcmf_awdl_fweh_event(struct brcmf_if *ifp,
+ const struct brcmf_event_msg *e, void *data)
+{
+ struct brcmf_pub *drvr = ifp->drvr;
+ struct sk_buff *skb;
+ u32 hdr;
+
+ if (!ifp->awdl)
+ return 0;
+
+ switch (e->event_code) {
+ case BRCMF_E_ACTION_FRAME_RX:
+ case BRCMF_E_AWDL_EVENT:
+ break;
+ case BRCMF_E_AWDL_ROLE:
+ skb = brcmf_awdl_event_alloc(drvr, BRCMF_VNDR_EVTS_AWDL_ROLE,
+ NLMSG_DEFAULT_SIZE);
+ if (!skb)
+ return -ENOMEM;
+ if (nla_put_u32(skb, BRCMF_AWDL_ATTR_ROLE, e->status)) {
+ kfree_skb(skb);
+ return -EMSGSIZE;
+ }
+ cfg80211_vendor_event(skb, GFP_KERNEL);
+ return 0;
+ default:
+ return 0;
+ }
+
+ if (e->datalen < 2)
+ return 0;
+ switch (get_unaligned_be16(data)) {
+ case 1:
+ hdr = BRCMF_AWDL_RX_HDR_V1_LEN;
+ break;
+ case 2:
+ hdr = BRCMF_AWDL_RX_HDR_V2_LEN;
+ break;
+ default:
+ return 0;
+ }
+ if (e->datalen < hdr + sizeof(brcmf_awdl_af) ||
+ memcmp((u8 *)data + hdr, brcmf_awdl_af, sizeof(brcmf_awdl_af)))
+ return 0;
+
+ skb = brcmf_awdl_event_alloc(drvr, BRCMF_VNDR_EVTS_AWDL_FRAME,
+ e->datalen);
+ if (!skb)
+ return -ENOMEM;
+ if (nla_put(skb, BRCMF_AWDL_ATTR_ADDR, ETH_ALEN, e->addr) ||
+ nla_put(skb, BRCMF_AWDL_ATTR_FRAME, e->datalen - hdr,
+ (u8 *)data + hdr)) {
+ kfree_skb(skb);
+ return -EMSGSIZE;
+ }
+ cfg80211_vendor_event(skb, GFP_KERNEL);
+ return 0;
+}
+
+/*
+ * Registers the firmware events that only the AWDL interface reports, so the
+ * event mask includes them. The firmware runs AWDL with all of them enabled.
+ */
+void brcmf_awdl_register_events(struct brcmf_pub *drvr)
+{
+ static const enum brcmf_fweh_event_code events[] = {
+ BRCMF_E_AWDL_AW, BRCMF_E_AWDL_ROLE, BRCMF_E_AWDL_EVENT,
+ BRCMF_E_AWDL_STATS, BRCMF_E_SDB_STATE,
+ };
+ struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
+ int i;
+
+ if (!pri || !brcmf_feat_is_enabled(pri, BRCMF_FEAT_AWDL))
+ return;
+ for (i = 0; i < ARRAY_SIZE(events); i++)
+ brcmf_fweh_register(drvr, events[i], brcmf_awdl_fweh_event);
+}
+
static void brcmf_awdl_send_result(struct brcmf_pub *drvr, int event,
int status, struct net_device *ndev)
{
- struct brcmf_if *pri = brcmf_get_ifp(drvr, 0);
struct sk_buff *skb;
- skb = cfg80211_vendor_event_alloc(drvr->wiphy,
- pri && pri->vif ? &pri->vif->wdev :
- NULL,
- NLMSG_DEFAULT_SIZE, event,
- GFP_KERNEL);
+ skb = brcmf_awdl_event_alloc(drvr, event, NLMSG_DEFAULT_SIZE);
if (!skb)
return;
if (nla_put_s32(skb, BRCMF_AWDL_ATTR_STATUS, status) ||
@@ -732,6 +840,8 @@ const struct nla_policy brcmf_awdl_vndr_policy[BRCMF_AWDL_ATTR_MAX + 1] = {
[BRCMF_AWDL_ATTR_IFINDEX] = { .type = NLA_REJECT },
[BRCMF_AWDL_ATTR_PEER_IES] = { .type = NLA_BINARY,
.len = BRCMF_AWDL_PEER_IES_MAX },
+ [BRCMF_AWDL_ATTR_FRAME] = { .type = NLA_REJECT },
+ [BRCMF_AWDL_ATTR_ROLE] = { .type = NLA_REJECT },
};
/* The device of a vendor command and its parsed attributes */
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
index 63030221dea..06c932a3c95 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/awdl.h
@@ -18,6 +18,7 @@ struct brcmf_pub;
struct brcmf_if;
struct net_device;
struct sk_buff;
+struct brcmf_event_msg;
enum brcmf_awdl_op {
BRCMF_AWDL_OP_NONE,
@@ -87,6 +88,9 @@ int brcmf_awdl_add(struct brcmf_pub *drvr, const u8 *mac,
struct net_device **ndev);
int brcmf_awdl_del(struct brcmf_pub *drvr);
void brcmf_awdl_suspend(struct brcmf_pub *drvr);
+void brcmf_awdl_register_events(struct brcmf_pub *drvr);
+int brcmf_awdl_fweh_event(struct brcmf_if *ifp,
+ const struct brcmf_event_msg *e, void *data);
bool brcmf_awdl_if_added(struct brcmf_pub *drvr, struct brcmf_if *ifp);
bool brcmf_awdl_if_removed(struct brcmf_pub *drvr, struct brcmf_if *ifp);
int brcmf_awdl_encap(struct brcmf_if *ifp, struct sk_buff *skb);
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
index 41ea0481cf1..cad12496173 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c
@@ -7045,6 +7045,7 @@ static s32 wl_init_priv(struct brcmf_cfg80211_info *cfg)
if (err)
return err;
brcmf_register_event_handlers(cfg);
+ brcmf_awdl_register_events(cfg->pub);
mutex_init(&cfg->usr_sync);
brcmf_init_escan(cfg);
brcmf_init_conf(cfg->conf);
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
index 8c098029104..bd4bf205cc1 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
@@ -324,11 +324,13 @@ static void brcmf_fweh_event_worker(struct work_struct *work)
ifp = drvr->iflist[0];
else
ifp = drvr->iflist[emsg.bsscfgidx];
- /* The AWDL interface has no vif, its events are not for
- * the cfg80211 handlers
+ /* The AWDL interface has no vif, its events go to user
+ * space and not to the cfg80211 handlers
*/
- if (ifp && ifp->awdl)
+ if (ifp && ifp->awdl) {
+ brcmf_awdl_fweh_event(ifp, &emsg, event->data);
goto event_free;
+ }
err = brcmf_fweh_call_event_handler(drvr, ifp, event->code,
&emsg, event->data);
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.h
index e327dd58d29..f8dee73cab1 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.h
@@ -94,7 +94,12 @@ struct brcmf_cfg80211_info;
BRCMF_ENUM_DEF(FIFO_CREDIT_MAP, 74) \
BRCMF_ENUM_DEF(ACTION_FRAME_RX, 75) \
BRCMF_ENUM_DEF(TDLS_PEER_EVENT, 92) \
+ BRCMF_ENUM_DEF(AWDL_AW, 96) \
+ BRCMF_ENUM_DEF(AWDL_ROLE, 97) \
+ BRCMF_ENUM_DEF(AWDL_EVENT, 98) \
BRCMF_ENUM_DEF(BCMC_CREDIT_SUPPORT, 127) \
+ BRCMF_ENUM_DEF(AWDL_STATS, 150) \
+ BRCMF_ENUM_DEF(SDB_STATE, 152) \
BRCMF_ABSTRACT_ENUM_DEF(EXT_AUTH_REQ, 0) \
BRCMF_ABSTRACT_ENUM_DEF(EXT_AUTH_FRAME_RX, 1) \
BRCMF_ABSTRACT_ENUM_DEF(MGMT_FRAME_TXSTATUS, 2) \
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
index 5e7de07c995..aae9cd75947 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.c
@@ -193,4 +193,12 @@ const struct nl80211_vendor_cmd_info brcmf_vendor_events[] = {
.vendor_id = BROADCOM_OUI,
.subcmd = BRCMF_VNDR_EVTS_AWDL_STOPPED
},
+ [BRCMF_VNDR_EVTS_AWDL_FRAME] = {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_EVTS_AWDL_FRAME
+ },
+ [BRCMF_VNDR_EVTS_AWDL_ROLE] = {
+ .vendor_id = BROADCOM_OUI,
+ .subcmd = BRCMF_VNDR_EVTS_AWDL_ROLE
+ },
};
diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
index ce7f7c46710..96009375eb7 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/vendor.h
@@ -54,6 +54,11 @@ enum brcmf_vndr_cmds {
* @BRCMF_VNDR_EVTS_AWDL_STOPPED: the AWDL interface was removed, after
* %BRCMF_VNDR_CMDS_AWDL_STOP or before system sleep, with
* %BRCMF_AWDL_ATTR_STATUS.
+ * @BRCMF_VNDR_EVTS_AWDL_FRAME: AWDL action frame received from
+ * %BRCMF_AWDL_ATTR_ADDR, the frame body from the category on in
+ * %BRCMF_AWDL_ATTR_FRAME.
+ * @BRCMF_VNDR_EVTS_AWDL_ROLE: the firmware changed its AWDL role to
+ * %BRCMF_AWDL_ATTR_ROLE.
* @BRCMF_VNDR_EVTS_LAST: number of vendor events.
*
* The value of an event is its index in the vendor events and its vendor
@@ -62,6 +67,8 @@ enum brcmf_vndr_cmds {
enum brcmf_vndr_evts {
BRCMF_VNDR_EVTS_AWDL_STARTED,
BRCMF_VNDR_EVTS_AWDL_STOPPED,
+ BRCMF_VNDR_EVTS_AWDL_FRAME,
+ BRCMF_VNDR_EVTS_AWDL_ROLE,
BRCMF_VNDR_EVTS_LAST
};
@@ -80,6 +87,9 @@ enum brcmf_vndr_evts {
* @BRCMF_AWDL_ATTR_IFINDEX: interface index of the AWDL interface (u32).
* @BRCMF_AWDL_ATTR_PEER_IES: HT and VHT capabilities elements of a peer,
* built from the AWDL TLVs it sends, as macOS passes them.
+ * @BRCMF_AWDL_ATTR_FRAME: received action frame body.
+ * @BRCMF_AWDL_ATTR_ROLE: AWDL role of the firmware (u32), 0 slave, 1 master
+ * not taking part in the election, 2 master.
* @BRCMF_AWDL_ATTR_MAX: highest attribute.
*/
enum brcmf_awdl_attrs {
@@ -94,6 +104,8 @@ enum brcmf_awdl_attrs {
BRCMF_AWDL_ATTR_STATUS,
BRCMF_AWDL_ATTR_IFINDEX,
BRCMF_AWDL_ATTR_PEER_IES,
+ BRCMF_AWDL_ATTR_FRAME,
+ BRCMF_AWDL_ATTR_ROLE,
__BRCMF_AWDL_ATTR_AFTER_LAST,
BRCMF_AWDL_ATTR_MAX = __BRCMF_AWDL_ATTR_AFTER_LAST - 1
^ permalink raw reply related [flat|nested] 8+ messages in thread