0
0
mirror of https://git.openwrt.org/openwrt/openwrt.git/ synced 2025-10-06 02:52:47 +02:00

wpa_supplicant: add MLO client support

Can also be used for a client mode interface that is able to connect on
multiple bands individually, while handling hostapd state for the correct
band.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau
2025-09-21 15:39:35 +02:00
parent 5170a2448c
commit 9aca8a97d7
7 changed files with 572 additions and 94 deletions

View File

@@ -248,6 +248,8 @@ export function generate(config_list, data, interface) {
iface: interface.config.ifname,
config: file_name,
'4addr': !!interface.config.wds,
mlo: !!interface.config.mlo,
freq_list: data.config.scan_list,
powersave: false
};

View File

@@ -17,6 +17,16 @@ let wdev_handler = {};
let wdev_script_task, wdev_script_timeout;
let handler_timer;
function supplicant_start_mlo()
{
ubus.call({
object: "wpa_supplicant",
method: "mld_start",
return: "ignore",
data: { },
});
}
function delete_wdev(name)
{
delete netifd.wireless.devices[name];
@@ -216,6 +226,9 @@ function run_next_handler()
{
while (!wdev_cur && length(wdev_handler) > 0)
__run_next_handler();
if (!wdev_cur && !length(wdev_handler))
supplicant_start_mlo();
}
function run_handler(wdev, op, cb)

View File

@@ -17,12 +17,12 @@ let wireless = netifd.wireless = {
path: realpath(netifd.main_path + "/wireless"),
};
function hostapd_update_mlo()
function wpad_update_mlo(service, mode)
{
let config = {};
for (let ifname, data in wireless.mlo) {
if (data.mode != "ap")
if (data.mode != mode)
continue;
data.phy = find_phy(data.radio_config[0], true);
@@ -33,17 +33,28 @@ function hostapd_update_mlo()
}
ubus.call({
object: "hostapd",
object: service,
method: "mld_set",
return: "ignore",
data: { config },
});
}
function hostapd_update_mlo()
{
wpad_update_mlo("hostapd", "ap");
}
function supplicant_update_mlo()
{
wpad_update_mlo("wpa_supplicant", "sta");
}
function update_config(new_devices, mlo_vifs)
{
wireless.mlo = mlo_vifs;
hostapd_update_mlo();
supplicant_update_mlo();
for (let name, dev in wireless.devices)
if (!new_devices[name])
@@ -516,6 +527,8 @@ wireless.obj = ubus.publish("network.wireless", ubus_obj);
wireless.listener = ubus.listener("ubus.object.add", (event, msg) => {
if (msg.path == "hostapd")
hostapd_update_mlo();
else if (msg.path == "wpa_supplicant")
supplicant_update_mlo();
});
return {

View File

@@ -13,6 +13,7 @@ function ex_handler(e)
}
libubus.guard(ex_handler);
wpas.data.mld = {};
wpas.data.config = {};
wpas.data.iface_phy = {};
wpas.data.macaddr_list = {};
@@ -77,6 +78,254 @@ function prepare_config(config, radio)
return { config };
}
function phy_dev_open(phy_name)
{
let phy = wpas.data.config[phy_name];
if (!phy) {
warn(`Missing phy config for ${phy_name}\n`);
return;
}
let phydev = phy_open(phy.name, phy.radio);
if (!phydev)
return;
let macaddr_list = wpas.data.macaddr_list[phy_name];
phydev.macaddr_init(macaddr_list, {
num_global: phy.num_global_macaddr,
macaddr_base: phy.macaddr_base,
});
return phydev;
}
function start_pending(phy_name)
{
let phy = wpas.data.config[phy_name];
if (!phy || !phy.data)
return;
let phydev = phy_dev_open(phy_name);
if (!phydev) {
wpas.printf(`Could not open phy ${phy_name}`);
return;
}
for (let ifname in phy.data)
iface_start(phydev, phy.data[ifname]);
}
function phy_name(phy, radio)
{
if (!phy)
return null;
if (radio != null && radio >= 0)
phy += "." + radio;
return phy;
}
function mld_remove(data)
{
if (!data.radio_mask_up)
return;
let name = data.name;
wpas.printf(`Remove MLD interface ${name}`);
wpas.remove_iface(name);
wdev_remove(name);
data.radio_mask_up = 0;
}
function mld_first_phy(data)
{
let mask = data.radio_mask_present;
for (let i = 0; mask; i++, mask >>= 1)
if (mask & 1)
return i;
}
function mld_radio_index(data, freq)
{
let phys = data.phy_config;
for (let i = 0; i < length(phys); i++)
if (phys[i] && index(phys[i].freq_list, freq) >= 0)
return i;
}
function mld_add(data, phy_list)
{
let name = data.name;
phy_list ??= [];
wpas.printf(`Add MLD interface ${name}`);
let radio = mld_first_phy(data);
if (radio == null)
return;
let phy_name = data.phy + '.' + radio;
let phydev = phy_list[phy_name];
if (!phydev) {
phydev = phy_dev_open(phy_name);
if (!phydev)
return;
phy_list[phy_name] = phydev;
}
let wdev_config = { ...data.config, radio_mask: data.radio_mask };
let ret = phydev.wdev_add(name, wdev_config);
if (ret)
wpas.printf(`Failed to create device ${name}: ${ret}`);
let first_config = data.phy_config[radio];
wdev_set_up(name, true);
wpas.add_iface(first_config);
let iface = wpas.interfaces[name];
if (!iface) {
wpas.printf(`Interface ${name} not found after adding\n`);
wpas.remove_iface(name);
wdev_remove(name);
return;
}
if (length(data.freq_list) > 0)
iface.config('freq_list', data.freq_list);
data.radio_mask_up = data.radio_mask_present;
}
function mld_remove_links(data)
{
// TODO
mld_remove(data);
}
function mld_add_links(data)
{
// TODO: incremental update
mld_remove(data);
mld_add(data);
}
function mld_set_config(config)
{
let prev_mld = { ...wpas.data.mld };
let new_mld = {};
let phy_list = {};
let new_config = !length(prev_mld);
wpas.printf(`Set MLD config: ${keys(config)}`);
for (let name, data in config) {
let prev = prev_mld[name];
if (prev && is_equal(prev.config, data)) {
new_mld[name] = prev;
delete prev_mld[name];
continue;
}
let radio_mask = 0;
for (let r in data.radios)
if (r != null)
radio_mask |= 1 << r;
new_mld[name] = {
name,
config: data,
phy: data.phy,
phy_config: [],
radio_mask,
radio_mask_up: 0,
radio_mask_present: 0,
};
}
for (let name, data in prev_mld)
mld_remove(data);
wpas.data.mld = new_mld;
}
function mld_set_iface_config(name, data, radio, config)
{
wpas.printf(`Set MLD interface ${name} radio ${radio} config: ${keys(config)}`);
data.phy_config[radio] = config;
if (config)
data.radio_mask_present |= 1 << radio;
else
data.radio_mask_present &= ~(1 << radio);
let freq_list;
for (let config in data.phy_config) {
if (!config || !config.freq_list)
continue;
if (!freq_list)
freq_list = [ ...config.freq_list ];
else
push(freq_list, ...config.freq_list);
}
data.freq_list = freq_list;
}
function mld_update_iface(name, data) {
if (!data.radio_mask_up)
return;
if (!data.radio_mask_present) {
mld_remove(data);
return;
}
let mask = data.radio_mask_up & ~data.radio_mask_present;
if (!mask)
return;
mld_remove_links(data);
}
function mld_update_phy(phy, ifaces) {
for (let name, data in wpas.data.mld) {
if (data.phy != phy.name)
continue;
mld_set_iface_config(name, data, phy.radio, ifaces[name]);
mld_update_iface(name, data);
}
}
function mld_start() {
wpas.printf(`Start pending MLD interfaces\n`);
let phy_list = {};
for (let name, data in wpas.data.mld) {
wpas.printf(`MLD interface ${name} present=${data.radio_mask_present} up=${data.radio_mask_up}`);
let add_mask = data.radio_mask_present & ~data.radio_mask_up;
if (!add_mask)
continue;
if (!data.radio_mask_up)
mld_add(data, phy_list);
else
mld_add_links(data);
}
}
function mld_bss_allowed(data, bss) {
if (!data.freq_list)
return true;
return index(data.freq_list, bss.freq) >= 0;
}
function set_config(config_name, phy_name, radio, num_global_macaddr, macaddr_base, config_list)
{
let phy = wpas.data.config[config_name];
@@ -95,47 +344,17 @@ function set_config(config_name, phy_name, radio, num_global_macaddr, macaddr_ba
phy.macaddr_base = macaddr_base;
let values = [];
let mlo_ifaces = {};
for (let config in config_list)
push(values, [ config.iface, prepare_config(config) ]);
if (config.mlo)
mlo_ifaces[config.iface] = config;
else
push(values, [ config.iface, prepare_config(config) ]);
mld_update_phy(phy, mlo_ifaces);
phy.update(values);
}
function start_pending(phy_name)
{
let phy = wpas.data.config[phy_name];
let ubus = wpas.data.ubus;
if (!phy || !phy.data)
return;
let phydev = phy_open(phy.name, phy.radio);
if (!phydev) {
wpas.printf(`Could not open phy ${phy_name}`);
return;
}
let macaddr_list = wpas.data.macaddr_list[phy_name];
phydev.macaddr_init(macaddr_list, {
num_global: phy.num_global_macaddr,
macaddr_base: phy.macaddr_base,
});
for (let ifname in phy.data)
iface_start(phydev, phy.data[ifname]);
}
function phy_name(phy, radio)
{
if (!phy)
return null;
if (radio != null && radio >= 0)
phy += "." + radio;
return phy;
}
let main_obj = {
phy_set_state: {
args: {
@@ -214,6 +433,25 @@ let main_obj = {
return libubus.STATUS_NOT_FOUND;
}
},
mld_set: {
args: {
config: {}
},
call: function(req) {
if (!req.args.config)
return libubus.STATUS_INVALID_ARGUMENT;
mld_set_config(req.args.config);
return 0;
}
},
mld_start: {
args: {},
call: function(req) {
mld_start();
return 0;
}
},
config_set: {
args: {
phy: "",
@@ -315,12 +553,30 @@ function iface_event(type, name, data) {
ubus.call("service", "event", { type: `wpa_supplicant.${name}.${type}`, data: {} });
}
function iface_hostapd_notify(phy, ifname, iface, state)
function iface_hostapd_fill_radio_link(mld, radio, msg, link)
{
let ubus = wpas.data.ubus;
let status = iface.status();
let msg = { phy: phy };
let config = mld.phy_config[radio];
if (!config)
return;
let freq_list = config.freq_list;
if (!freq_list)
return;
if (!link || index(freq_list, link.frequency) < 0)
return;
msg.frequency = link.frequency;
msg.sec_chan_offset = link.sec_chan_offset;
}
function iface_hostapd_notify(ifname, iface, state)
{
let status = iface.status();
let ubus = wpas.data.ubus;
let msg = {};
let mld = wpas.data.mld[ifname];
switch (state) {
case "DISCONNECTED":
case "AUTHENTICATING":
@@ -333,26 +589,76 @@ function iface_hostapd_notify(phy, ifname, iface, state)
break;
case "COMPLETED":
msg.up = true;
msg.frequency = status.frequency;
msg.sec_chan_offset = status.sec_chan_offset;
if (!mld) {
msg.frequency = status.frequency;
msg.sec_chan_offset = status.sec_chan_offset;
}
break;
default:
return;
}
ubus.call("hostapd", "apsta_state", msg);
if (!mld) {
msg.phy = wpas.data.iface_phy[ifname];
if (!phy) {
wpas.printf(`no PHY for ifname ${ifname}`);
return;
}
ubus.call("hostapd", "apsta_state", msg);
return;
}
let radio_mask = mld.radio_mask;
for (let i = 0; radio_mask; i++, radio_mask >>= 1) {
if (!(radio_mask & 1)) {
wpas.printf(`skip radio ${i}`);
continue;
}
let radio_msg = {
...msg,
phy: mld.phy,
radio: i,
};
if (state == "COMPLETED") {
if (status.links)
for (let link in status.links)
iface_hostapd_fill_radio_link(mld, i, radio_msg, link);
else
iface_hostapd_fill_radio_link(mld, i, radio_msg, status);
}
ubus.call("hostapd", "apsta_state", radio_msg);
}
}
function iface_channel_switch(phy, ifname, iface, info)
function iface_channel_switch(ifname, iface, info)
{
let msg = {
phy: phy,
up: true,
csa: true,
csa_count: info.csa_count ? info.csa_count - 1 : 0,
frequency: info.frequency,
sec_chan_offset: info.sec_chan_offset,
};
let mld = wpas.data.mld[ifname];
if (mld) {
msg.phy = mld.phy;
msg.radio = mld_radio_index(mld, info.frequency);
if (msg.radio == null) {
wpas.printf(`PHY ${mld.phy} radio for frequency ${info.frequency} not found`);
return;
}
} else {
msg.phy = wpas.data.iface_phy[ifname];
if (!msg.phy) {
wpas.printf(`no PHY for ifname ${ifname}`);
return;
}
}
ubus.call("hostapd", "apsta_state", msg);
}
@@ -362,6 +668,13 @@ return {
set_config(phy, []);
wpas.ubus.disconnect();
},
bss_allowed: function(ifname, bss) {
let mld = wpas.data.mld[ifname];
if (!mld)
return true;
return mld_bss_allowed(mld, bss);
},
iface_add: function(name, obj) {
iface_event("add", name);
},
@@ -369,39 +682,35 @@ return {
iface_event("remove", name);
},
state: function(ifname, iface, state) {
let phy = wpas.data.iface_phy[ifname];
if (!phy) {
wpas.printf(`no PHY for ifname ${ifname}`);
return;
try {
iface_hostapd_notify(ifname, iface, state);
if (state != "COMPLETED")
return;
let phy = wpas.data.iface_phy[ifname];
if (!phy)
return;
let phy_data = wpas.data.config[phy];
if (!phy_data)
return;
let iface_data = phy_data.data[ifname];
if (!iface_data)
return;
let wdev_config = iface_data.config;
if (!wdev_config || wdev_config.mode != "mesh")
return;
wdev_set_mesh_params(ifname, wdev_config);
} catch (e) {
ex_handler(e);
}
iface_hostapd_notify(phy, ifname, iface, state);
if (state != "COMPLETED")
return;
let phy_data = wpas.data.config[phy];
if (!phy_data)
return;
let iface_data = phy_data.data[ifname];
if (!iface_data)
return;
let wdev_config = iface_data.config;
if (!wdev_config || wdev_config.mode != "mesh")
return;
wdev_set_mesh_params(ifname, wdev_config);
},
event: function(ifname, iface, ev, info) {
let phy = wpas.data.iface_phy[ifname];
if (!phy) {
wpas.printf(`no PHY for ifname ${ifname}`);
return;
}
if (ev == "CH_SWITCH_STARTED")
iface_channel_switch(phy, ifname, iface, info);
iface_channel_switch(ifname, iface, info);
}
};

View File

@@ -702,7 +702,28 @@ as adding/removing interfaces.
CFLAGS += -DEAP_SERVER -DEAP_SERVER_IDENTITY
--- a/wpa_supplicant/events.c
+++ b/wpa_supplicant/events.c
@@ -6293,6 +6293,7 @@ void supplicant_event(void *ctx, enum wp
@@ -53,6 +53,7 @@
#include "wmm_ac.h"
#include "nan_usd.h"
#include "dpp_supplicant.h"
+#include "ucode.h"
#define MAX_OWE_TRANSITION_BSS_SELECT_COUNT 5
@@ -1706,6 +1707,12 @@ struct wpa_ssid * wpa_scan_res_match(str
return NULL;
}
+ if (!wpas_ucode_bss_allowed(wpa_s, bss)) {
+ if (debug_print)
+ wpa_dbg(wpa_s, MSG_DEBUG, " skip - denied by ucode handler");
+ return NULL;
+ }
+
for (ssid = group; ssid; ssid = only_first_ssid ? NULL : ssid->pnext) {
if (wpa_scan_res_ok(wpa_s, ssid, match_ssid, match_ssid_len,
bss, bssid_ignore_count, debug_print, link))
@@ -6293,6 +6300,7 @@ void supplicant_event(void *ctx, enum wp
event_to_string(event), event);
#endif /* CONFIG_NO_STDOUT_DEBUG */

View File

@@ -6,6 +6,7 @@
#include "wpa_supplicant_i.h"
#include "wps_supplicant.h"
#include "ctrl_iface.h"
#include "config.h"
#include "bss.h"
#include "ucode.h"
@@ -41,6 +42,21 @@ wpas_ucode_update_interfaces(void)
ucv_object_add(ucv_prototype_get(global), "interfaces", ifs);
}
static uc_value_t *
wpas_ucode_bss_get_uval(struct wpa_bss *bss)
{
uc_value_t *val;
val = ucv_object_new(vm);
ucv_object_add(val, "freq", ucv_int64_new(bss->freq));
ucv_object_add(val, "ssid", ucv_string_new_length(bss->ssid, bss->ssid_len));
ucv_object_add(val, "snr", ucv_int64_new(bss->snr));
ucv_object_add(val, "signal", ucv_int64_new(bss->level));
ucv_object_add(val, "noise", ucv_int64_new(bss->noise));
return val;
}
void wpas_ucode_add_bss(struct wpa_supplicant *wpa_s)
{
uc_value_t *val;
@@ -71,6 +87,25 @@ void wpas_ucode_free_bss(struct wpa_supplicant *wpa_s)
ucv_put(val);
}
bool wpas_ucode_bss_allowed(struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
uc_value_t *val;
bool ret = true;
if (wpa_ucode_call_prepare("bss_allowed"))
return true;
uc_value_push(ucv_string_new(wpa_s->ifname));
uc_value_push(wpas_ucode_bss_get_uval(bss));
val = wpa_ucode_call(2);
if (ucv_type(val) == UC_BOOLEAN)
ret = ucv_boolean_get(val);
ucv_put(val);
return ret;
}
void wpas_ucode_update_state(struct wpa_supplicant *wpa_s)
{
const char *state;
@@ -203,6 +238,29 @@ out:
return ucv_int64_new(ret);
}
static void
uc_wpas_iface_status_bss(uc_value_t *ret, struct wpa_bss *bss)
{
int sec_chan = 0;
const u8 *ie;
ie = wpa_bss_get_ie(bss, WLAN_EID_HT_OPERATION);
if (ie && ie[1] >= 2) {
const struct ieee80211_ht_operation *ht_oper;
int sec;
ht_oper = (const void *) (ie + 2);
sec = ht_oper->ht_param & HT_INFO_HT_PARAM_SECONDARY_CHNL_OFF_MASK;
if (sec == HT_INFO_HT_PARAM_SECONDARY_CHNL_ABOVE)
sec_chan = 1;
else if (sec == HT_INFO_HT_PARAM_SECONDARY_CHNL_BELOW)
sec_chan = -1;
}
ucv_object_add(ret, "sec_chan_offset", ucv_int64_new(sec_chan));
ucv_object_add(ret, "frequency", ucv_int64_new(bss->freq));
}
static uc_value_t *
uc_wpas_iface_status(uc_vm_t *vm, size_t nargs)
{
@@ -218,25 +276,29 @@ uc_wpas_iface_status(uc_vm_t *vm, size_t nargs)
ucv_object_add(ret, "state", ucv_string_new(wpa_supplicant_state_txt(wpa_s->wpa_state)));
bss = wpa_s->current_bss;
if (bss) {
int sec_chan = 0;
const u8 *ie;
if (bss)
uc_wpas_iface_status_bss(ret, bss);
ie = wpa_bss_get_ie(bss, WLAN_EID_HT_OPERATION);
if (ie && ie[1] >= 2) {
const struct ieee80211_ht_operation *ht_oper;
int sec;
if (wpa_s->valid_links) {
unsigned int valid_links = wpa_s->valid_links;
uc_value_t *link, *links;
ht_oper = (const void *) (ie + 2);
sec = ht_oper->ht_param & HT_INFO_HT_PARAM_SECONDARY_CHNL_OFF_MASK;
if (sec == HT_INFO_HT_PARAM_SECONDARY_CHNL_ABOVE)
sec_chan = 1;
else if (sec == HT_INFO_HT_PARAM_SECONDARY_CHNL_BELOW)
sec_chan = -1;
links = ucv_array_new(vm);
for (size_t i = 0;
valid_links && i < ARRAY_SIZE(wpa_s->links);
i++, valid_links >>= 1) {
bss = wpa_s->links[i].bss;
if (!(valid_links & 1) || !bss)
continue;
link = ucv_object_new(vm);
uc_wpas_iface_status_bss(link, bss);
ucv_array_set(links, i, link);
}
ucv_object_add(ret, "sec_chan_offset", ucv_int64_new(sec_chan));
ucv_object_add(ret, "frequency", ucv_int64_new(bss->freq));
ucv_object_add(ret, "links", links);
}
#ifdef CONFIG_MESH
@@ -276,6 +338,58 @@ uc_wpas_iface_ctrl(uc_vm_t *vm, size_t nargs)
return ret;
}
static uc_value_t *
uc_wpas_iface_config(uc_vm_t *vm, size_t nargs)
{
struct wpa_supplicant *wpa_s = uc_fn_thisval("wpas.iface");
uc_value_t *arg = uc_fn_arg(0);
uc_value_t *val = uc_fn_arg(1);
uc_value_t *ret = NULL;
bool get = nargs == 1;
const char *name;
size_t len = 0;
if (!wpa_s || ucv_type(arg) != UC_STRING)
return NULL;
name = ucv_string_get(arg);
if (!strcmp(name, "freq_list")) {
if (get) {
int *cur = wpa_s->conf->freq_list;
if (!cur)
return NULL;
ret = ucv_array_new(vm);
while (*cur)
ucv_array_set(ret, len++, ucv_int64_new(*(cur++)));
} else {
size_t len = ucv_array_length(val);
int *freq_list;
if (ucv_type(val) != UC_ARRAY)
return NULL;
freq_list = calloc(len + 1, sizeof(*freq_list));
for (size_t i = 0; i < len; i++) {
uc_value_t *cur = ucv_array_get(val, i);
if (ucv_type(cur) != UC_INTEGER) {
free(freq_list);
return NULL;
}
freq_list[i] = ucv_int64_get(cur);
}
free(wpa_s->conf->freq_list);
wpa_s->conf->freq_list = freq_list;
ret = ucv_boolean_new(true);
}
}
return ret;
}
int wpas_ucode_init(struct wpa_global *gl)
{
static const uc_function_list_t global_fns[] = {
@@ -288,6 +402,7 @@ int wpas_ucode_init(struct wpa_global *gl)
static const uc_function_list_t iface_fns[] = {
{ "status", uc_wpas_iface_status },
{ "ctrl", uc_wpas_iface_ctrl },
{ "config", uc_wpas_iface_config },
};
uc_value_t *data, *proto;

View File

@@ -20,6 +20,7 @@ void wpas_ucode_add_bss(struct wpa_supplicant *wpa_s);
void wpas_ucode_free_bss(struct wpa_supplicant *wpa_s);
void wpas_ucode_update_state(struct wpa_supplicant *wpa_s);
void wpas_ucode_event(struct wpa_supplicant *wpa_s, int event, union wpa_event_data *data);
bool wpas_ucode_bss_allowed(struct wpa_supplicant *wpa_s, struct wpa_bss *bss);
#else
static inline int wpas_ucode_init(struct wpa_global *gl)
{
@@ -44,6 +45,10 @@ static inline void wpas_ucode_event(struct wpa_supplicant *wpa_s, int event, uni
{
}
static inline bool wpas_ucode_bss_allowed(struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
{
return true;
}
#endif
#endif