3
0
mirror of https://git.kernel.org/pub/scm/network/wireless/iwd.git synced 2024-10-06 03:18:46 +02:00
iwd/src/offchannel.c
James Prestwood 49b9eae18c offchannel: handle out of order ACKs/events
Its been seen (so far only in mac80211_hwsim + UML) where an
offchannel requests ACK comes after the ROC started event. This
causes the ROC started event to never call back to notify since
info->roc_cookie is unset and it appears to be coming from an
external process.

We can detect this situation in the ROC notify event by checking
if there is a pending ROC command and if info->roc_cookie does
not match. This can also be true for an external event so we just
set a new "early_cookie" member and return.

Then, when the ACK comes in for the ROC request, we can validate
if the prior event was associated with IWD or some external
process. If it was from IWD call the started callback, otherwise
the ROC notify event should come later and handled under the
normal logic where the cookies match.
2023-10-26 09:30:03 -05:00

362 lines
8.8 KiB
C

/*
*
* Wireless daemon for Linux
*
* Copyright (C) 2021 Intel Corporation. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <ell/ell.h>
#include "linux/nl80211.h"
#include "src/offchannel.h"
#include "src/wiphy.h"
#include "src/nl80211util.h"
#include "src/iwd.h"
#include "src/module.h"
struct offchannel_info {
uint64_t wdev_id;
uint32_t freq;
uint32_t duration;
uint32_t roc_cmd_id;
uint64_t roc_cookie;
uint64_t early_cookie;
offchannel_started_cb_t started;
offchannel_destroy_cb_t destroy;
void *user_data;
int error;
struct wiphy_radio_work_item work;
bool needs_cancel : 1;
};
static struct l_genl_family *nl80211;
static struct l_queue *offchannel_list;
static bool match_id(const void *a, const void *user_data)
{
const struct offchannel_info *info = a;
uint32_t id = L_PTR_TO_UINT(user_data);
return id == info->work.id;
}
static void offchannel_cancel_roc(struct offchannel_info *info)
{
struct l_genl_msg *msg;
msg = l_genl_msg_new(NL80211_CMD_CANCEL_REMAIN_ON_CHANNEL);
l_genl_msg_append_attr(msg, NL80211_ATTR_WDEV, 8, &info->wdev_id);
l_genl_msg_append_attr(msg, NL80211_ATTR_COOKIE, 8, &info->roc_cookie);
/* Nothing much can be done if this fails */
if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL))
l_genl_msg_unref(msg);
}
static void offchannel_roc_cb(struct l_genl_msg *msg, void *user_data)
{
struct offchannel_info *info = user_data;
info->error = l_genl_msg_get_error(msg);
info->roc_cmd_id = 0;
if (info->error < 0) {
l_debug("Error from CMD_REMAIN_ON_CHANNEL (%d)", info->error);
goto work_done;
}
info->error = nl80211_parse_attrs(msg, NL80211_ATTR_COOKIE,
&info->roc_cookie, NL80211_ATTR_UNSPEC);
if (info->error < 0) {
l_error("Could not parse ROC cookie");
goto work_done;
}
/*
* If the request was cancelled prior to kernel sending the ACK,
* cancel now.
*
* If the ROC event came before the ACK, call back now since the
* callback was skipped in the notify event. There is the potential that
* an external process issued the ROC, but if the cookies don't match
* here we can be sure it wasn't for us. In this case the notify event
* will behave as normal and call started().
*/
if (info->needs_cancel)
offchannel_cancel_roc(info);
else if (info->early_cookie == info->roc_cookie && info->started)
info->started(info->user_data);
return;
work_done:
wiphy_radio_work_done(wiphy_find_by_wdev(info->wdev_id), info->work.id);
}
static bool offchannel_work_ready(struct wiphy_radio_work_item *item)
{
struct l_genl_msg *msg;
struct offchannel_info *info = l_container_of(item,
struct offchannel_info, work);
msg = l_genl_msg_new(NL80211_CMD_REMAIN_ON_CHANNEL);
l_genl_msg_append_attr(msg, NL80211_ATTR_WDEV, 8, &info->wdev_id);
l_genl_msg_append_attr(msg, NL80211_ATTR_WIPHY_FREQ, 4, &info->freq);
l_genl_msg_append_attr(msg, NL80211_ATTR_DURATION, 4, &info->duration);
info->roc_cmd_id = l_genl_family_send(nl80211, msg, offchannel_roc_cb,
info, NULL);
if (!info->roc_cmd_id) {
info->error = -EIO;
l_genl_msg_unref(msg);
return true;
}
l_queue_push_head(offchannel_list, info);
return false;
}
static void offchannel_work_destroy(struct wiphy_radio_work_item *item)
{
struct offchannel_info *info = l_container_of(item,
struct offchannel_info, work);
if (info->destroy)
info->destroy(info->error, info->user_data);
l_queue_remove(offchannel_list, info);
l_free(info);
}
static const struct wiphy_radio_work_item_ops offchannel_work_ops = {
.do_work = offchannel_work_ready,
.destroy = offchannel_work_destroy,
};
uint32_t offchannel_start(uint64_t wdev_id, int priority, uint32_t freq,
uint32_t duration, offchannel_started_cb_t started,
void *user_data, offchannel_destroy_cb_t destroy)
{
struct offchannel_info *info = l_new(struct offchannel_info, 1);
info->wdev_id = wdev_id;
info->freq = freq;
info->duration = duration;
info->started = started;
info->destroy = destroy;
info->user_data = user_data;
/*
* Set error as cancelled in case this work gets cancelled prior to
* the wiphy work starting.
*/
info->error = -ECANCELED;
return wiphy_radio_work_insert(wiphy_find_by_wdev(wdev_id), &info->work,
priority, &offchannel_work_ops);
}
void offchannel_cancel(uint64_t wdev_id, uint32_t id)
{
struct wiphy *wiphy = wiphy_find_by_wdev(wdev_id);
struct offchannel_info *info;
int ret;
if (!wiphy)
return;
/*
* Exit if work does not exist, if it hasn't started 'done' the work,
* otherwise decide how the work needs to be canceled.
*/
ret = wiphy_radio_work_is_running(wiphy, id);
if (ret < 0)
return;
else if (ret == false)
goto work_done;
info = l_queue_find(offchannel_list, match_id, L_UINT_TO_PTR(id));
if (!info)
return;
if (info->roc_cmd_id) {
/*
* If the command hasn't left the genl queue it can be cancelled
* without any further action. Otherwise command has been sent
* to the kernel and we must wait until ROC starts and cancel at
* that time.
*/
if (!l_genl_family_request_sent(nl80211, info->roc_cmd_id)) {
l_genl_family_cancel(nl80211, info->roc_cmd_id);
info->roc_cmd_id = 0;
goto work_done;
}
/* Lets the ROC callback know it needs to cancel the request */
info->needs_cancel = true;
goto destroy;
}
/*
* Something weird must have happened on the kernel side. This error
* will already be handled in offchannel_roc_cb but warn here to inform
* the user.
*/
if (L_WARN_ON(!info->roc_cookie))
return;
/*
* At this point we know ROC has at least been queued (potentially not
* started) and can be cancelled. The work will be completed once the
* kernel sends the cancel ROC event.
*/
offchannel_cancel_roc(info);
destroy:
if (info->destroy)
info->destroy(-ECANCELED, info->user_data);
info->destroy = NULL;
info->started = NULL;
info->user_data = NULL;
return;
work_done:
wiphy_radio_work_done(wiphy, id);
}
static void offchannel_mlme_notify(struct l_genl_msg *msg, void *user_data)
{
struct offchannel_info *info = NULL;
const struct l_queue_entry *e;
uint64_t wdev_id;
uint64_t cookie;
uint8_t cmd;
cmd = l_genl_msg_get_command(msg);
if (cmd != NL80211_CMD_REMAIN_ON_CHANNEL &&
cmd != NL80211_CMD_CANCEL_REMAIN_ON_CHANNEL)
return;
if (nl80211_parse_attrs(msg, NL80211_ATTR_WDEV, &wdev_id,
NL80211_ATTR_COOKIE, &cookie,
NL80211_ATTR_UNSPEC) < 0)
return;
for (e = l_queue_get_entries(offchannel_list); e; e = e->next) {
struct offchannel_info *i = e->data;
if (i->wdev_id != wdev_id)
continue;
/* Normal case, kernel has ACK'ed */
if (i->roc_cookie == cookie) {
info = i;
break;
}
/*
* If there is a pending request and no ACK yet this could be:
* - an early event coming prior to the ACK
* - an event coming from an external ROC request (we just
* happened to have also sent an ROC request).
*
* We can't tell where the event originated until we recieve our
* ACK so set early_cookie to track it.
*/
if (i->roc_cmd_id != 0 && l_genl_family_request_sent(nl80211,
i->roc_cmd_id)) {
i->early_cookie = cookie;
return;
}
}
if (!info)
return;
switch (cmd) {
case NL80211_CMD_REMAIN_ON_CHANNEL:
if (info->started)
info->started(info->user_data);
break;
case NL80211_CMD_CANCEL_REMAIN_ON_CHANNEL:
info->error = 0;
wiphy_radio_work_done(wiphy_find_by_wdev(info->wdev_id),
info->work.id);
break;
default:
return;
}
}
static int offchannel_init(void)
{
struct l_genl *genl = iwd_get_genl();
nl80211 = l_genl_family_new(genl, NL80211_GENL_NAME);
if (!nl80211) {
l_error("Failed to obtain nl80211");
return -EIO;
}
if (!l_genl_family_register(nl80211, "mlme", offchannel_mlme_notify,
NULL, NULL)) {
l_error("Failed to register for MLME");
l_genl_family_free(nl80211);
nl80211 = NULL;
return -EIO;
}
offchannel_list = l_queue_new();
return 0;
}
static void offchannel_exit(void)
{
l_debug("");
l_genl_family_free(nl80211);
nl80211 = NULL;
l_queue_destroy(offchannel_list, l_free);
}
IWD_MODULE(offchannel, offchannel_init, offchannel_exit);
IWD_MODULE_DEPENDS(offchannel, wiphy);