]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
hostapd: ucode: restart on config-initiated channel switch failure
authorFelix Fietkau <nbd@nbd.name>
Fri, 10 Jul 2026 11:13:32 +0000 (13:13 +0200)
committerFelix Fietkau <nbd@nbd.name>
Thu, 16 Jul 2026 09:50:21 +0000 (11:50 +0200)
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>
package/network/services/hostapd/files/hostapd.uc
package/network/services/hostapd/src/src/ap/ucode.c

index 2eea5d731becaf8eb9d802f7d263994b4b33563a..3bb016e5dc6d424a5e8e65fb8208752dddce2f64 100644 (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;
 }
 
index 4db5eddf7c5212b7e663cc6bb707a67fefa153b3..8af8d5cfdeac1b30ef34a2173d6f9e897af26704 100644 (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;