hostapd: ucode: restart on config-initiated channel switch failure

iface.switch_channel() only reports whether a CSA was started, not whether
it completed, so a config-driven channel switch that the driver never
finishes would leave the AP stuck without recovery. After issuing the CSA,
arm a timer and, if the switch has not completed by the deadline
(csa_in_progress still set), fall back to a full interface restart. The
timer is armed only on the config-initiated path, so ubus- and
apsta-triggered channel switches keep their own recovery.

Add an iface.csa_in_progress() ucode method wrapping hostapd_csa_in_progress().

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau 2026-07-10 13:13:32 +02:00
parent 55b9263596
commit c6d20bbf51
2 changed files with 61 additions and 0 deletions

View file

@ -1,4 +1,5 @@
let libubus = require("ubus");
import * as uloop from "uloop";
import { open, readfile, access } from "fs";
import { wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open, wdev_set_radio_mask, wdev_set_up } from "common";
@ -320,11 +321,22 @@ function iface_macaddr_init(phydev, config, macaddr_list)
return phydev.macaddr_init(macaddr_list, macaddr_data);
}
function csa_timer_cancel(name)
{
let timers = hostapd.data.csa_timer;
if (timers && timers[name]) {
timers[name].cancel();
delete timers[name];
}
}
function iface_restart(phydev, config, old_config)
{
let phy = phydev.name;
let pending = hostapd.data.pending_config[phy];
csa_timer_cancel(phy);
if (pending)
pending.abort();
@ -580,6 +592,22 @@ function iface_channel_is_dfs(radio)
(freq >= 5490 && freq <= 5730));
}
function iface_csa_check(name)
{
if (hostapd.data.csa_timer)
delete hostapd.data.csa_timer[name];
let config = hostapd.data.config[name];
let iface = hostapd.interfaces[name];
if (!config || !iface || !iface.csa_in_progress())
return;
hostapd.printf(`Config channel switch on phy ${name} did not complete, restarting`);
let phydev = phy_open(config.phy, config.radio_idx);
if (phydev)
iface_restart(phydev, config, config);
}
function iface_channel_switch(name, config)
{
let radio = config.radio;
@ -618,6 +646,27 @@ function iface_channel_switch(name, config)
return false;
hostapd.printf(`Channel switch to ${radio.channel} on phy ${name}`);
/*
* Verify the switch actually completes; if the driver never finishes it,
* fall back to a full restart. Armed only here, so ubus- or apsta-
* triggered channel switches are left to their own recovery.
*/
hostapd.data.csa_timer ??= {};
csa_timer_cancel(name);
let beacon_int = 100;
for (let line in config.radio.data) {
let m = match(line, /^beacon_int=([0-9]+)/);
if (m) {
beacon_int = int(m[1]);
break;
}
}
hostapd.data.csa_timer[name] = uloop.timer(freq_info.csa_count * beacon_int + 2000,
() => iface_csa_check(name));
return true;
}

View file

@ -769,6 +769,17 @@ uc_hostapd_iface_switch_channel(uc_vm_t *vm, size_t nargs)
return ucv_boolean_new(!ret);
}
static uc_value_t *
uc_hostapd_iface_csa_in_progress(uc_vm_t *vm, size_t nargs)
{
struct hostapd_iface *iface = uc_fn_thisval("hostapd.iface");
if (!iface)
return NULL;
return ucv_boolean_new(hostapd_csa_in_progress(iface));
}
static uc_value_t *
uc_hostapd_bss_rename(uc_vm_t *vm, size_t nargs)
{
@ -1225,6 +1236,7 @@ int hostapd_ucode_init(struct hapd_interfaces *ifaces)
{ "stop", uc_hostapd_iface_stop },
{ "start", uc_hostapd_iface_start },
{ "switch_channel", uc_hostapd_iface_switch_channel },
{ "csa_in_progress", uc_hostapd_iface_csa_in_progress },
};
uc_value_t *data, *proto;