From: Felix Fietkau Date: Fri, 10 Jul 2026 11:13:32 +0000 (+0200) Subject: hostapd: ucode: restart on config-initiated channel switch failure X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c6d20bbf51996e21b7eb4688be0512d5032f236d;p=thirdparty%2Fopenwrt.git 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 --- diff --git a/package/network/services/hostapd/files/hostapd.uc b/package/network/services/hostapd/files/hostapd.uc index 2eea5d731be..3bb016e5dc6 100644 --- a/package/network/services/hostapd/files/hostapd.uc +++ b/package/network/services/hostapd/files/hostapd.uc @@ -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; } diff --git a/package/network/services/hostapd/src/src/ap/ucode.c b/package/network/services/hostapd/src/src/ap/ucode.c index 4db5eddf7c5..8af8d5cfdea 100644 --- a/package/network/services/hostapd/src/src/ap/ucode.c +++ b/package/network/services/hostapd/src/src/ap/ucode.c @@ -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;