[openwrt/openwrt] wifi-scripts: add multi-radio config support

LEDE Commits lede-commits at lists.infradead.org
Tue Oct 22 05:49:10 PDT 2024


nbd pushed a commit to openwrt/openwrt.git, branch main:
https://git.openwrt.org/04fb05914ea77db9a0be2ef82249bdfd7c80fc65

commit 04fb05914ea77db9a0be2ef82249bdfd7c80fc65
Author: Felix Fietkau <nbd at nbd.name>
AuthorDate: Wed Jun 12 15:06:05 2024 +0200

    wifi-scripts: add multi-radio config support
    
    Emit one wifi-device section per wiphy radio
    
    Signed-off-by: Felix Fietkau <nbd at nbd.name>
---
 .../files/lib/netifd/wireless/mac80211.sh          |  72 +++++-----
 .../config/wifi-scripts/files/lib/wifi/mac80211.uc |  90 ++++++------
 .../wifi-scripts/files/usr/share/hostap/common.uc  | 101 ++++++++++----
 .../wifi-scripts/files/usr/share/hostap/wdev.uc    |  37 +++--
 .../files/usr/share/hostap/wifi-detect.uc          |  47 +++++++
 package/network/services/hostapd/files/hostapd.uc  | 153 ++++++++++++++-------
 .../services/hostapd/files/wpa_supplicant.uc       |  59 +++++---
 7 files changed, 370 insertions(+), 189 deletions(-)

diff --git a/package/network/config/wifi-scripts/files/lib/netifd/wireless/mac80211.sh b/package/network/config/wifi-scripts/files/lib/netifd/wireless/mac80211.sh
index 23e557105b..9998e26ec1 100755
--- a/package/network/config/wifi-scripts/files/lib/netifd/wireless/mac80211.sh
+++ b/package/network/config/wifi-scripts/files/lib/netifd/wireless/mac80211.sh
@@ -29,7 +29,7 @@ drv_mac80211_init_device_config() {
 	config_add_string path phy 'macaddr:macaddr'
 	config_add_string tx_burst
 	config_add_string distance
-	config_add_int beacon_int chanbw frag rts
+	config_add_int radio beacon_int chanbw frag rts
 	config_add_int rxantenna txantenna txpower min_tx_power
 	config_add_int num_global_macaddr multiple_bssid
 	config_add_boolean noscan ht_coex acs_exclude_dfs background_radar
@@ -556,7 +556,7 @@ mac80211_hostapd_setup_bss() {
 	}
 	[ "$staidx" -gt 0 -o "$start_disabled" -eq 1 ] && append hostapd_cfg "start_disabled=1" "$N"
 
-	cat >> /var/run/hostapd-$phy.conf <<EOF
+	cat >> /var/run/hostapd-$phy$vif_phy_suffix.conf <<EOF
 $hostapd_cfg
 bssid=$macaddr
 ${default_macaddr:+#default_macaddr}
@@ -576,7 +576,7 @@ mac80211_generate_mac() {
 	local phy="$1"
 	local id="${macidx:-0}"
 
-	wdev_tool "$phy" get_macaddr id=$id num_global=$num_global_macaddr mbssid=${multiple_bssid:-0}
+	wdev_tool "$phy$phy_suffix" get_macaddr id=$id num_global=$num_global_macaddr mbssid=${multiple_bssid:-0}
 }
 
 get_board_phy_name() (
@@ -679,7 +679,7 @@ mac80211_prepare_vif() {
 		monitor) prefix=mon;;
 		esac
 
-		mac80211_set_ifname "$phy" "$prefix"
+		mac80211_set_ifname "$phy$vif_phy_suffix" "$prefix"
 	}
 
 	append active_ifnames "$ifname"
@@ -880,7 +880,12 @@ mac80211_set_vif_txpower() {
 	json_get_vars vif_txpower
 	json_select ..
 
-	[ -z "$vif_txpower" ] || iw dev "$ifname" set txpower fixed "${vif_txpower%%.*}00"
+	set_default vif_txpower "$txpower"
+	if [ -n "$vif_txpower" ]; then
+		iw dev "$ifname" set txpower fixed "${vif_txpower%%.*}00"
+	else
+		iw dev "$ifname" set txpower auto
+	fi
 }
 
 wpa_supplicant_init_config() {
@@ -920,11 +925,13 @@ wpa_supplicant_add_interface() {
 
 wpa_supplicant_set_config() {
 	local phy="$1"
+	local radio="$2"
 	local prev
 
 	json_set_namespace wpa_supp prev
 	json_close_array
 	json_add_string phy "$phy"
+	json_add_int radio "$radio"
 	json_add_int num_global_macaddr "$num_global_macaddr"
 	json_add_boolean defer 1
 	local data="$(json_dump)"
@@ -947,13 +954,16 @@ wpa_supplicant_set_config() {
 }
 
 hostapd_set_config() {
+	local phy="$1"
+	local radio="$2"
+
 	[ -n "$hostapd_ctrl" ] || {
-		ubus_call hostapd config_set '{ "phy": "'"$phy"'", "config": "", "prev_config": "'"${hostapd_conf_file}.prev"'" }' > /dev/null
+		ubus_call hostapd config_set '{ "phy": "'"$phy"'", "radio": '"$radio"', "config": "", "prev_config": "'"${hostapd_conf_file}.prev"'" }' > /dev/null
 		return 0;
 	}
 
 	ubus wait_for hostapd
-	local hostapd_res="$(ubus_call hostapd config_set "{ \"phy\": \"$phy\", \"config\":\"${hostapd_conf_file}\", \"prev_config\": \"${hostapd_conf_file}.prev\"}")"
+	local hostapd_res="$(ubus_call hostapd config_set "{ \"phy\": \"$phy\", \"radio\": $radio, \"config\":\"${hostapd_conf_file}\", \"prev_config\": \"${hostapd_conf_file}.prev\"}")"
 	ret="$?"
 	[ "$ret" != 0 -o -z "$hostapd_res" ] && {
 		wireless_setup_failed HOSTAPD_START_FAILED
@@ -965,10 +975,11 @@ hostapd_set_config() {
 
 wpa_supplicant_start() {
 	local phy="$1"
+	local radio="$2"
 
 	[ -n "$wpa_supp_init" ] || return 0
 
-	ubus_call wpa_supplicant config_set '{ "phy": "'"$phy"'", "num_global_macaddr": '"$num_global_macaddr"' }' > /dev/null
+	ubus_call wpa_supplicant config_set '{ "phy": "'"$phy"'", "radio": '"$radio"', "num_global_macaddr": '"$num_global_macaddr"' }' > /dev/null
 }
 
 mac80211_setup_supplicant() {
@@ -1073,18 +1084,23 @@ drv_mac80211_cleanup() {
 }
 
 mac80211_reset_config() {
-	local phy="$1"
+	hostapd_conf_file="/var/run/hostapd-$phy$vif_phy_suffix.conf"
+	ubus_call hostapd config_set '{ "phy": "'"$phy"'", "radio": '"$radio"', "config": "", "prev_config": "'"$hostapd_conf_file"'" }' > /dev/null
+	ubus_call wpa_supplicant config_set '{ "phy": "'"$phy"'", "radio": '"$radio"', "config": [] }' > /dev/null
+	wdev_tool "$phy$phy_suffix" set_config '{}'
+}
 
-	hostapd_conf_file="/var/run/hostapd-$phy.conf"
-	ubus_call hostapd config_set '{ "phy": "'"$phy"'", "config": "", "prev_config": "'"$hostapd_conf_file"'" }' > /dev/null
-	ubus_call wpa_supplicant config_set '{ "phy": "'"$phy"'", "config": [] }' > /dev/null
-	wdev_tool "$phy" set_config '{}'
+mac80211_set_suffix() {
+	[ "$radio" = "-1" ] && radio=
+	phy_suffix="${radio:+:$radio}"
+	vif_phy_suffix="${radio:+.$radio}"
+	set_default radio -1
 }
 
 drv_mac80211_setup() {
 	json_select config
 	json_get_vars \
-		phy macaddr path \
+		radio phy macaddr path \
 		country chanbw distance \
 		txpower \
 		rxantenna txantenna \
@@ -1094,6 +1110,8 @@ drv_mac80211_setup() {
 	json_get_values scan_list scan_list
 	json_select ..
 
+	mac80211_set_suffix
+
 	json_select data && {
 		json_get_var prev_rxantenna rxantenna
 		json_get_var prev_txantenna txantenna
@@ -1120,7 +1138,7 @@ drv_mac80211_setup() {
 		}
 	}
 
-	hostapd_conf_file="/var/run/hostapd-$phy.conf"
+	hostapd_conf_file="/var/run/hostapd-$phy$vif_phy_suffix.conf"
 
 	macidx=0
 	staidx=0
@@ -1139,17 +1157,11 @@ drv_mac80211_setup() {
 	[ "$rxantenna" = "all" ] && rxantenna=0xffffffff
 
 	[ "$rxantenna" = "$prev_rxantenna" -a "$txantenna" = "$prev_txantenna" ] || mac80211_reset_config "$phy"
-	wireless_set_data phy="$phy" txantenna="$txantenna" rxantenna="$rxantenna"
+	wireless_set_data phy="$phy" radio="$radio" txantenna="$txantenna" rxantenna="$rxantenna"
 
 	iw phy "$phy" set antenna $txantenna $rxantenna >/dev/null 2>&1
 	iw phy "$phy" set distance "$distance" >/dev/null 2>&1
 
-	if [ -n "$txpower" ]; then
-		iw phy "$phy" set txpower fixed "${txpower%%.*}00"
-	else
-		iw phy "$phy" set txpower auto
-	fi
-
 	[ -n "$frag" ] && iw phy "$phy" set frag "${frag%%.*}"
 	[ -n "$rts" ] && iw phy "$phy" set rts "${rts%%.*}"
 
@@ -1177,13 +1189,13 @@ drv_mac80211_setup() {
 	for_each_interface "ap sta adhoc mesh monitor" mac80211_prepare_vif
 	for_each_interface "ap sta adhoc mesh monitor" mac80211_setup_vif
 
-	[ -x /usr/sbin/wpa_supplicant ] && wpa_supplicant_set_config "$phy"
-	[ -x /usr/sbin/hostapd ] && hostapd_set_config "$phy"
+	[ -x /usr/sbin/wpa_supplicant ] && wpa_supplicant_set_config "$phy" "$radio"
+	[ -x /usr/sbin/hostapd ] && hostapd_set_config "$phy" "$radio"
 
-	[ -x /usr/sbin/wpa_supplicant ] && wpa_supplicant_start "$phy"
+	[ -x /usr/sbin/wpa_supplicant ] && wpa_supplicant_start "$phy" "$radio"
 
 	json_set_namespace wdev_uc prev
-	wdev_tool "$phy" set_config "$(json_dump)" $active_ifnames
+	wdev_tool "$phy$phy_suffix" set_config "$(json_dump)" $active_ifnames
 	json_set_namespace "$prev"
 
 	for_each_interface "ap sta adhoc mesh monitor" mac80211_set_vif_txpower
@@ -1210,19 +1222,15 @@ list_phy_interfaces() {
 
 drv_mac80211_teardown() {
 	json_select data
-	json_get_vars phy
+	json_get_vars phy radio
 	json_select ..
 	[ -n "$phy" ] || {
 		echo "Bug: PHY is undefined for device '$1'"
 		return 1
 	}
 
+	mac80211_set_suffix
 	mac80211_reset_config "$phy"
-
-	for wdev in $(list_phy_interfaces "$phy"); do
-		ip link set dev "$wdev" down
-		iw dev "$wdev" del
-	done
 }
 
 add_driver mac80211
diff --git a/package/network/config/wifi-scripts/files/lib/wifi/mac80211.uc b/package/network/config/wifi-scripts/files/lib/wifi/mac80211.uc
index 9fd6c35e8b..e82525e247 100644
--- a/package/network/config/wifi-scripts/files/lib/wifi/mac80211.uc
+++ b/package/network/config/wifi-scripts/files/lib/wifi/mac80211.uc
@@ -14,10 +14,12 @@ let commit;
 
 let config = uci.cursor().get_all("wireless") ?? {};
 
-function radio_exists(path, macaddr, phy) {
+function radio_exists(path, macaddr, phy, radio) {
 	for (let name, s in config) {
 		if (s[".type"] != "wifi-device")
 			continue;
+		if (radio != null && int(s.radio) != radio)
+			continue;
 		if (s.macaddr & lc(s.macaddr) == lc(macaddr))
 			return true;
 		if (s.phy == phy)
@@ -34,55 +36,61 @@ for (let phy_name, phy in board.wlan) {
 	if (!info || !length(info.bands))
 		continue;
 
-	while (config[`radio${idx}`])
-		idx++;
-	let name = "radio" + idx++;
+	let radios = length(info.radios) > 0 ? info.radios : [{ bands: info.bands }];
+	for (let radio in radios) {
+		while (config[`radio${idx}`])
+			idx++;
+		let name = "radio" + idx;
 
-	let s = "wireless." + name;
-	let si = "wireless.default_" + name;
+		let s = "wireless." + name;
+		let si = "wireless.default_" + name;
 
-	let band_name = filter(bands_order, (b) => info.bands[b])[0];
-	if (!band_name)
-		continue;
+		let band_name = filter(bands_order, (b) => radio.bands[b])[0];
+		if (!band_name)
+			continue;
 
-	let band = info.bands[band_name];
-	let channel = band.default_channel ?? "auto";
+		let band = info.bands[band_name];
+		let rband = radio.bands[band_name];
+		let channel = rband.default_channel ?? "auto";
 
-	let width = band.max_width;
-	if (band_name == "2G")
-		width = 20;
-	else if (width > 80)
-		width = 80;
+		let width = band.max_width;
+		if (band_name == "2G")
+			width = 20;
+		else if (width > 80)
+			width = 80;
 
-	let htmode = filter(htmode_order, (m) => band[lc(m)])[0];
-	if (htmode)
-		htmode += width;
-	else
-		htmode = "NOHT";
+		let htmode = filter(htmode_order, (m) => band[lc(m)])[0];
+		if (htmode)
+			htmode += width;
+		else
+			htmode = "NOHT";
 
-	if (!phy.path)
-		continue;
+		if (!phy.path)
+			continue;
 
-	let macaddr = trim(readfile(`/sys/class/ieee80211/${phy_name}/macaddress`));
-	if (radio_exists(phy.path, macaddr, phy_name))
-		continue;
+		let macaddr = trim(readfile(`/sys/class/ieee80211/${phy_name}/macaddress`));
+		if (radio_exists(phy.path, macaddr, phy_name, radio.index))
+			continue;
 
-	let id = `phy='${phy_name}'`;
-	if (match(phy_name, /^phy[0-9]/))
-		id = `path='${phy.path}'`;
+		let id = `phy='${phy_name}'`;
+		if (match(phy_name, /^phy[0-9]/))
+			id = `path='${phy.path}'`;
 
-	band_name = lc(band_name);
+		band_name = lc(band_name);
 
-	let country, defaults, num_global_macaddr;
-	if (board.wlan.defaults) {
-		defaults = board.wlan.defaults.ssids?.[band_name]?.ssid ? board.wlan.defaults.ssids?.[band_name] : board.wlan.defaults.ssids?.all;
-		country = board.wlan.defaults.country;
-		if (!country && band_name != '2g')
-			defaults = null;
-		num_global_macaddr = board.wlan.defaults.ssids?.[band_name]?.mac_count;
-	}
+		let country, defaults, num_global_macaddr;
+		if (board.wlan.defaults) {
+			defaults = board.wlan.defaults.ssids?.[band_name]?.ssid ? board.wlan.defaults.ssids?.[band_name] : board.wlan.defaults.ssids?.all;
+			country = board.wlan.defaults.country;
+			if (!country && band_name != '2g')
+				defaults = null;
+			num_global_macaddr = board.wlan.defaults.ssids?.[band_name]?.mac_count;
+		}
 
-	print(`set ${s}=wifi-device
+		if (length(info.radios) > 0)
+			id += `\nset ${s}.radio='${radio.index}'`;
+
+		print(`set ${s}=wifi-device
 set ${s}.type='mac80211'
 set ${s}.${id}
 set ${s}.band='${band_name}'
@@ -101,7 +109,9 @@ set ${si}.encryption='${defaults?.encryption || "none"}'
 set ${si}.key='${defaults?.key || ""}'
 
 `);
-	commit = true;
+		config[name] = {};
+		commit = true;
+	}
 }
 
 if (commit)
diff --git a/package/network/config/wifi-scripts/files/usr/share/hostap/common.uc b/package/network/config/wifi-scripts/files/usr/share/hostap/common.uc
index 750e3ae71c..c6bfb62ef3 100644
--- a/package/network/config/wifi-scripts/files/usr/share/hostap/common.uc
+++ b/package/network/config/wifi-scripts/files/usr/share/hostap/common.uc
@@ -1,6 +1,6 @@
 import * as nl80211 from "nl80211";
 import * as rtnl from "rtnl";
-import { readfile, glob, basename, readlink } from "fs";
+import { readfile, glob, basename, readlink, open } from "fs";
 
 const iftypes = {
 	ap: nl80211.const.NL80211_IFTYPE_AP,
@@ -74,6 +74,14 @@ function find_reusable_wdev(phyidx)
 	return null;
 }
 
+function wdev_set_radio_mask(name, mask)
+{
+	nl80211.request(nl80211.const.NL80211_CMD_SET_INTERFACE, 0, {
+		dev: name,
+		vif_radio_mask: mask
+	});
+}
+
 function wdev_create(phy, name, data)
 {
 	let phyidx = int(readfile(`/sys/class/ieee80211/${phy}/index`));
@@ -93,24 +101,24 @@ function wdev_create(phy, name, data)
 		req["4addr"] = data["4addr"];
 	if (data.macaddr)
 		req.mac = data.macaddr;
+	if (data.radio != null && data.radio >= 0)
+		req.vif_radio_mask = 1 << data.radio;
 
 	nl80211.error();
 
 	let reuse_ifname = find_reusable_wdev(phyidx);
 	if (reuse_ifname &&
 	    (reuse_ifname == name ||
-	     rtnl.request(rtnl.const.RTM_SETLINK, 0, { dev: reuse_ifname, ifname: name}) != false))
-		nl80211.request(
-			nl80211.const.NL80211_CMD_SET_INTERFACE, 0, {
-				wiphy: phyidx,
-				dev: name,
-				iftype: iftypes[data.mode],
-			});
-	else
+	     rtnl.request(rtnl.const.RTM_SETLINK, 0, { dev: reuse_ifname, ifname: name}) != false)) {
+		req.dev = req.ifname;
+		delete req.ifname;
+		nl80211.request(nl80211.const.NL80211_CMD_SET_INTERFACE, 0, req);
+	} else {
 		nl80211.request(
 			nl80211.const.NL80211_CMD_NEW_INTERFACE,
 			nl80211.const.NLM_F_CREATE,
 			req);
+	}
 
 	let error = nl80211.error();
 	if (error)
@@ -190,7 +198,8 @@ const phy_proto = {
 	},
 
 	macaddr_generate: function(data) {
-		let phy = this.name;
+		let phy = this.phy;
+		let radio_idx = this.radio;
 		let idx = int(data.id ?? 0);
 		let mbssid = int(data.mbssid ?? 0) > 0;
 		let num_global = int(data.num_global ?? 1);
@@ -200,22 +209,30 @@ const phy_proto = {
 		if (!base_addr)
 			return null;
 
-		if (!idx && !mbssid)
-			return base_addr;
-
 		let base_mask = phy_sysfs_file(phy, "address_mask");
 		if (!base_mask)
 			return null;
 
-		if (base_mask == "00:00:00:00:00:00" && idx >= num_global) {
+		if (base_mask == "00:00:00:00:00:00" &&
+		    (radio_idx > 0 || idx >= num_global)) {
 			let addrs = split(phy_sysfs_file(phy, "addresses"), "\n");
 
-			if (idx < length(addrs))
-				return addrs[idx];
+			if (radio_idx != null) {
+				if (radio_idx && radio_idx < length(addrs))
+					base_addr = addrs[radio_idx];
+				else
+					idx += radio_idx * 16;
+			} else {
+				if (idx < length(addrs))
+					return addrs[idx];
 
-			base_mask = "ff:ff:ff:ff:ff:ff";
+				base_mask = "ff:ff:ff:ff:ff:ff";
+			}
 		}
 
+		if (!idx && !mbssid)
+			return base_addr;
+
 		let addr = macaddr_split(base_addr);
 		let mask = macaddr_split(base_mask);
 		let type;
@@ -275,27 +292,55 @@ const phy_proto = {
 		}
 	},
 
+	wdev_add: function(name, data) {
+		let phydev = this;
+		wdev_create(this.phy, name, {
+			...data,
+			radio: this.radio,
+		});
+	},
+
 	for_each_wdev: function(cb) {
-		let wdevs = glob(`/sys/class/ieee80211/${this.name}/device/net/*`);
-		wdevs = map(wdevs, (arg) => basename(arg));
+		let wdevs = nl80211.request(
+			nl80211.const.NL80211_CMD_GET_INTERFACE,
+			nl80211.const.NLM_F_DUMP,
+			{ wiphy: this.idx }
+		);
+
+		let mac_wdev = {};
 		for (let wdev in wdevs) {
-			if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != this.name)
+			if (wdev.iftype == nl80211.const.NL80211_IFTYPE_AP_VLAN)
+				continue;
+			if (this.radio != null && wdev.vif_radio_mask != null &&
+			    !(wdev.vif_radio_mask & (1 << this.radio)))
 				continue;
+			mac_wdev[wdev.mac] = wdev;
+		}
 
-			cb(wdev);
+		for (let wdev in wdevs) {
+			if (!mac_wdev[wdev.mac])
+				continue;
+
+			cb(wdev.ifname);
 		}
 	}
 };
 
-function phy_open(phy)
+function phy_open(phy, radio)
 {
 	let phyidx = readfile(`/sys/class/ieee80211/${phy}/index`);
 	if (!phyidx)
 		return null;
 
+	let name = phy;
+	if (radio === "" || radio < 0)
+		radio = null;
+	if (radio != null)
+		name += "." + radio;
+
 	return proto({
-		name: phy,
-		idx: int(phyidx)
+		phy, name, radio,
+		idx: int(phyidx),
 	}, phy_proto);
 }
 
@@ -365,9 +410,9 @@ function is_equal(val1, val2) {
 
 function vlist_new(cb) {
 	return proto({
-			cb: cb,
-			data: {}
-		}, vlist_proto);
+		cb: cb,
+		data: {}
+	}, vlist_proto);
 }
 
-export { wdev_remove, wdev_create, wdev_set_mesh_params, wdev_set_up, is_equal, vlist_new, phy_is_fullmac, phy_open };
+export { wdev_remove, wdev_create, wdev_set_mesh_params, wdev_set_radio_mask, wdev_set_up, is_equal, vlist_new, phy_is_fullmac, phy_open };
diff --git a/package/network/config/wifi-scripts/files/usr/share/hostap/wdev.uc b/package/network/config/wifi-scripts/files/usr/share/hostap/wdev.uc
index d505e3035f..d836a930fc 100644
--- a/package/network/config/wifi-scripts/files/usr/share/hostap/wdev.uc
+++ b/package/network/config/wifi-scripts/files/usr/share/hostap/wdev.uc
@@ -1,13 +1,13 @@
 #!/usr/bin/env ucode
 'use strict';
-import { vlist_new, is_equal, wdev_create, wdev_set_mesh_params, wdev_remove, wdev_set_up, phy_open } from "/usr/share/hostap/common.uc";
+import { vlist_new, is_equal, wdev_set_mesh_params, wdev_remove, wdev_set_up, phy_open } from "/usr/share/hostap/common.uc";
 import { readfile, writefile, basename, readlink, glob } from "fs";
 let libubus = require("ubus");
 
 let keep_devices = {};
-let phy = shift(ARGV);
+let phy_name = shift(ARGV);
 let command = shift(ARGV);
-let phydev;
+let phy, phydev;
 
 function iface_stop(wdev)
 {
@@ -30,7 +30,7 @@ function iface_start(wdev)
 		wdev_config[key] = wdev[key];
 	if (!wdev_config.macaddr && wdev.mode != "monitor")
 		wdev_config.macaddr = phydev.macaddr_next();
-	wdev_create(phy, ifname, wdev_config);
+	phydev.wdev_add(ifname, wdev_config);
 	wdev_set_up(ifname, true);
 	let htmode = wdev.htmode || "NOHT";
 	if (wdev.freq)
@@ -85,20 +85,15 @@ function delete_ifname(config)
 		delete config[key].ifname;
 }
 
-function add_existing(phy, config)
+function add_existing(phydev, config)
 {
-	let wdevs = glob(`/sys/class/ieee80211/${phy}/device/net/*`);
-	wdevs = map(wdevs, (arg) => basename(arg));
-	for (let wdev in wdevs) {
+	phydev.for_each_wdev((wdev) => {
 		if (config[wdev])
-			continue;
-
-		if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != phy)
-			continue;
+			return;
 
 		if (trim(readfile(`/sys/class/net/${wdev}/operstate`)) == "down")
 			config[wdev] = {};
-	}
+	});
 }
 
 function usage()
@@ -114,7 +109,7 @@ Commands:
 
 const commands = {
 	set_config: function(args) {
-		let statefile = `/var/run/wdev-${phy}.json`;
+		let statefile = `/var/run/wdev-${phy_name}.json`;
 
 		let new_config = shift(args);
 		for (let dev in ARGV)
@@ -137,12 +132,12 @@ const commands = {
 		if (type(old_config) == "object")
 			config.data = old_config;
 
-		add_existing(phy, config.data);
+		add_existing(phydev, config.data);
 		add_ifname(config.data);
 		drop_inactive(config.data);
 
 		let ubus = libubus.connect();
-		let data = ubus.call("hostapd", "config_get_macaddr_list", { phy: phy });
+		let data = ubus.call("hostapd", "config_get_macaddr_list", { phy: phydev.name, radio: phydev.radio ?? -1 });
 		let macaddr_list = [];
 		if (type(data) == "object" && data.macaddr)
 			macaddr_list = data.macaddr;
@@ -166,7 +161,7 @@ const commands = {
 
 		let macaddr = phydev.macaddr_generate(data);
 		if (!macaddr) {
-			warn(`Could not get MAC address for phy ${phy}\n`);
+			warn(`Could not get MAC address for phy ${phy_name}\n`);
 			exit(1);
 		}
 
@@ -174,12 +169,14 @@ const commands = {
 	},
 };
 
-if (!phy || !command | !commands[command])
+if (!phy_name || !command | !commands[command])
 	usage();
 
-phydev = phy_open(phy);
+let phy_split = split(phy_name, ":");
+phydev = phy_open(phy_split[0], phy_split[1]);
+phy = phydev.phy;
 if (!phydev) {
-	warn(`PHY ${phy} does not exist\n`);
+	warn(`PHY ${phy_name} does not exist\n`);
 	exit(1);
 }
 
diff --git a/package/network/config/wifi-scripts/files/usr/share/hostap/wifi-detect.uc b/package/network/config/wifi-scripts/files/usr/share/hostap/wifi-detect.uc
index ee9155b3d0..db862d40c2 100644
--- a/package/network/config/wifi-scripts/files/usr/share/hostap/wifi-detect.uc
+++ b/package/network/config/wifi-scripts/files/usr/share/hostap/wifi-detect.uc
@@ -73,6 +73,15 @@ function freq_to_channel(freq) {
 	return 0;
 }
 
+function freq_range_match(ranges, freq) {
+	freq *= 1000;
+	for (let range in ranges) {
+		if (freq >= range[0] && freq <= range[1])
+			return true;
+	}
+	return false;
+}
+
 function wiphy_detect() {
 	let phys = nl.request(nl.const.NL80211_CMD_GET_WIPHY, nl.const.NLM_F_DUMP, { split_wiphy_dump: true });
 	if (!phys)
@@ -88,8 +97,27 @@ function wiphy_detect() {
 			antenna_rx: phy.wiphy_antenna_avail_rx,
 			antenna_tx: phy.wiphy_antenna_avail_tx,
 			bands: {},
+			radios: []
 		};
 
+		for (let radio in phy.radios) {
+			// S1G is not supported yet
+			radio.freq_ranges = filter(radio.freq_ranges,
+				(range) => range.end > 2000000
+			);
+
+			if (!length(radio.freq_ranges))
+				continue;
+
+			push(info.radios, {
+				index: radio.index,
+				freq_ranges: map(radio.freq_ranges,
+					(range) => [ range.start, range.end ]
+				),
+				bands: {}
+			});
+		}
+
 		let bands = info.bands;
 		for (let band in phy.wiphy_bands) {
 			if (!band || !band.freqs)
@@ -160,6 +188,25 @@ function wiphy_detect() {
 			if (eht_phy_cap && he_phy_cap & 2)
 				push(modes, "EHT40");
 
+			for (let radio in info.radios) {
+				let freq_match = filter(band.freqs,
+					(freq) => freq_range_match(radio.freq_ranges, freq.freq)
+				);
+				if (!length(freq_match))
+					continue;
+
+				let radio_band = {};
+				radio.bands[band_name] = radio_band;
+
+				freq_match = filter(freq_match,
+					(freq) => !freq.disabled
+				);
+
+				let freq = freq_match[0];
+				if (freq)
+					radio_band.default_channel = freq_to_channel(freq.freq);
+			}
+
 			for (let freq in band.freqs) {
 				if (freq.disabled)
 					continue;
diff --git a/package/network/services/hostapd/files/hostapd.uc b/package/network/services/hostapd/files/hostapd.uc
index e666ad4faa..2d9ce28749 100644
--- a/package/network/services/hostapd/files/hostapd.uc
+++ b/package/network/services/hostapd/files/hostapd.uc
@@ -1,6 +1,6 @@
 let libubus = require("ubus");
 import { open, readfile } from "fs";
-import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open } from "common";
+import { wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open, wdev_set_radio_mask } from "common";
 
 let ubus = libubus.connect(null, 60);
 
@@ -56,7 +56,7 @@ function iface_remove(cfg)
 		wdev_remove(bss.ifname);
 }
 
-function iface_gen_config(phy, config, start_disabled)
+function iface_gen_config(config, start_disabled)
 {
 	let str = `data:
 ${join("\n", config.radio.data)}
@@ -117,7 +117,7 @@ function iface_freq_info(iface, config, params)
 
 function iface_add(phy, config, phy_status)
 {
-	let config_inline = iface_gen_config(phy, config, !!phy_status);
+	let config_inline = iface_gen_config(config, !!phy_status);
 
 	let bss = config.bss[0];
 	let ret = hostapd.add_iface(`bss_config=${phy}:${config_inline}`);
@@ -148,12 +148,16 @@ function iface_config_macaddr_list(config)
 	return macaddr_list;
 }
 
-function iface_update_supplicant_macaddr(phy, config)
+function iface_update_supplicant_macaddr(phydev, config)
 {
 	let macaddr_list = [];
 	for (let i = 0; i < length(config.bss); i++)
 		push(macaddr_list, config.bss[i].bssid);
-	ubus.defer("wpa_supplicant", "phy_set_macaddr_list", { phy: phy, macaddr: macaddr_list });
+	ubus.defer("wpa_supplicant", "phy_set_macaddr_list", {
+		phy: phydev.name,
+		radio: phydev.radio ?? -1,
+		macaddr: macaddr_list
+	});
 }
 
 function __iface_pending_next(pending, state, ret, data)
@@ -168,19 +172,22 @@ function __iface_pending_next(pending, state, ret, data)
 	delete pending.defer;
 	switch (state) {
 	case "init":
-		let macaddr_list = [];
-		for (let i = 0; i < length(config.bss); i++)
-			push(macaddr_list, config.bss[i].bssid);
-		pending.call("wpa_supplicant", "phy_set_macaddr_list", { phy: phy, macaddr: macaddr_list });
+		iface_update_supplicant_macaddr(phydev, config);
 		return "create_bss";
 	case "create_bss":
-		let err = wdev_create(phy, bss.ifname, { mode: "ap" });
+		let err = phydev.wdev_add(bss.ifname, {
+			mode: "ap",
+			radio: phydev.radio,
+		});
 		if (err) {
 			hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
 			return null;
 		}
 
-		pending.call("wpa_supplicant", "phy_status", { phy: phy });
+		pending.call("wpa_supplicant", "phy_status", {
+			phy: phydev.phy,
+			radio: phydev.radio,
+		});
 		return "check_phy";
 	case "check_phy":
 		let phy_status = data;
@@ -190,12 +197,20 @@ function __iface_pending_next(pending, state, ret, data)
 
 			hostapd.printf(`Failed to bring up phy ${phy} ifname=${bss.ifname} with supplicant provided frequency`);
 		}
-		pending.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
+		pending.call("wpa_supplicant", "phy_set_state", {
+			phy: phydev.phy,
+			radio: phydev.radio,
+			stop: true
+		});
 		return "wpas_stopped";
 	case "wpas_stopped":
 		if (!iface_add(phy, config))
 			hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
-		pending.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
+		pending.call("wpa_supplicant", "phy_set_state", {
+			phy: phydev.phy,
+			radio: phydev.radio,
+			stop: false
+		});
 		return null;
 	case "done":
 	default:
@@ -210,9 +225,14 @@ function iface_pending_next(ret, data)
 	let cfg = this;
 
 	while (pending) {
-		this.next_state = __iface_pending_next(cfg, this.next_state, ret, data);
-		if (!this.next_state) {
-			__iface_pending_next(cfg, "done");
+		try {
+			this.next_state = __iface_pending_next(cfg, this.next_state, ret, data);
+			if (!this.next_state) {
+				__iface_pending_next(cfg, "done");
+				return;
+			}
+		} catch(e) {
+			hostapd.printf(`Exception: ${e}\n${e.stacktrace[0].context}`);
 			return;
 		}
 		pending = !this.defer;
@@ -398,7 +418,7 @@ function get_config_bss(config, idx)
 	return hostapd.bss[ifname];
 }
 
-function iface_reload_config(phydev, config, old_config)
+function iface_reload_config(name, phydev, config, old_config)
 {
 	let phy = phydev.name;
 
@@ -408,13 +428,13 @@ function iface_reload_config(phydev, config, old_config)
 	if (is_equal(old_config.bss, config.bss))
 		return true;
 
-	if (hostapd.data.pending_config[phy])
+	if (hostapd.data.pending_config[name])
 		return false;
 
 	if (!old_config.bss || !old_config.bss[0])
 		return false;
 
-	let iface = hostapd.interfaces[phy];
+	let iface = hostapd.interfaces[name];
 	let iface_name = old_config.bss[0].ifname;
 	if (!iface) {
 		hostapd.printf(`Could not find previous interface ${iface_name}`);
@@ -509,7 +529,7 @@ function iface_reload_config(phydev, config, old_config)
 			return false;
 
 		let ifname = old_config.bss[i].ifname;
-		hostapd.printf(`Remove bss '${ifname}' on phy '${phy}'`);
+		hostapd.printf(`Remove bss '${ifname}' on phy '${name}'`);
 		prev_bss.delete();
 		wdev_remove(ifname);
 	}
@@ -574,13 +594,13 @@ function iface_reload_config(phydev, config, old_config)
 
 		let addr = phydev.macaddr_next(i);
 		if (!addr) {
-			hostapd.printf(`Failed to generate mac address for phy ${phy}`);
+			hostapd.printf(`Failed to generate mac address for phy ${name}`);
 			return false;
 		}
 		bsscfg.bssid = addr;
 	}
 
-	let config_inline = iface_gen_config(phy, config);
+	let config_inline = iface_gen_config(config);
 
 	// Step 7: fill in the gaps with new interfaces
 	for (let i = 0; i < length(config.bss); i++) {
@@ -590,17 +610,17 @@ function iface_reload_config(phydev, config, old_config)
 		if (bss)
 			continue;
 
-		hostapd.printf(`Add bss ${ifname} on phy ${phy}`);
+		hostapd.printf(`Add bss ${ifname} on phy ${name}`);
 		bss_list[i] = iface.add_bss(config_inline, i);
 		if (!bss_list[i]) {
-			hostapd.printf(`Failed to add new bss ${ifname} on phy ${phy}`);
+			hostapd.printf(`Failed to add new bss ${ifname} on phy ${name}`);
 			return false;
 		}
 	}
 
 	// Step 8: update interface bss order
 	if (!iface.set_bss_order(bss_list)) {
-		hostapd.printf(`Failed to update BSS order on phy '${phy}'`);
+		hostapd.printf(`Failed to update BSS order on phy '${name}'`);
 		return false;
 	}
 
@@ -631,7 +651,7 @@ function iface_reload_config(phydev, config, old_config)
 		if (is_equal(config.bss[i], bss_list_cfg[i]))
 			continue;
 
-		hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
+		hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${name}'`);
 		if (bss.set_config(config_inline, i) < 0) {
 			hostapd.printf(`Failed to set config for bss ${ifname}`);
 			return false;
@@ -641,35 +661,36 @@ function iface_reload_config(phydev, config, old_config)
 	return true;
 }
 
-function iface_set_config(phy, config)
+function iface_set_config(name, config)
 {
-	let old_config = hostapd.data.config[phy];
+	let old_config = hostapd.data.config[name];
 
-	hostapd.data.config[phy] = config;
+	hostapd.data.config[name] = config;
 
 	if (!config) {
-		hostapd.remove_iface(phy);
+		hostapd.remove_iface(name);
 		return iface_remove(old_config);
 	}
 
-	let phydev = phy_open(phy);
+	let phy = config.phy;
+	let phydev = phy_open(phy, config.radio_idx);
 	if (!phydev) {
 		hostapd.printf(`Failed to open phy ${phy}`);
 		return false;
 	}
 
 	try {
-		let ret = iface_reload_config(phydev, config, old_config);
+		let ret = iface_reload_config(name, phydev, config, old_config);
 		if (ret) {
-			iface_update_supplicant_macaddr(phy, config);
-			hostapd.printf(`Reloaded settings for phy ${phy}`);
+			iface_update_supplicant_macaddr(phydev, config);
+			hostapd.printf(`Reloaded settings for phy ${name}`);
 			return 0;
 		}
 	} catch (e) {
-			hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
+		hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
 	}
 
-	hostapd.printf(`Restart interface for phy ${phy}`);
+	hostapd.printf(`Restart interface for phy ${name}`);
 	let ret = iface_restart(phydev, config, old_config);
 
 	return ret;
@@ -688,13 +709,18 @@ function config_add_bss(config, name)
 	return bss;
 }
 
-function iface_load_config(filename)
+function iface_load_config(phy, radio, filename)
 {
 	let f = open(filename, "r");
 	if (!f)
 		return null;
 
+	if (radio < 0)
+		radio = null;
+
 	let config = {
+		phy,
+		radio_idx: radio,
 		radio: {
 			data: []
 		},
@@ -769,6 +795,17 @@ function ex_wrap(func) {
 	};
 }
 
+function phy_name(phy, radio)
+{
+	if (!phy)
+		return null;
+
+	if (radio != null && radio >= 0)
+		phy += "." + radio;
+
+	return phy;
+}
+
 function bss_config(bss_name) {
 	for (let phy, config in hostapd.data.config) {
 		if (!config)
@@ -784,12 +821,13 @@ let main_obj = {
 	reload: {
 		args: {
 			phy: "",
+			radio: 0,
 		},
 		call: ex_wrap(function(req) {
-			let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
+			let phy_list = req.args.phy ? [ phy_name(req.args.phy, req.args.radio) ] : keys(hostapd.data.config);
 			for (let phy_name in phy_list) {
 				let phy = hostapd.data.config[phy_name];
-				let config = iface_load_config(phy.orig_file);
+				let config = iface_load_config(phy.phy, radio, phy.orig_file);
 				iface_set_config(phy_name, config);
 			}
 
@@ -799,6 +837,7 @@ let main_obj = {
 	apsta_state: {
 		args: {
 			phy: "",
+			radio: 0,
 			up: true,
 			frequency: 0,
 			sec_chan_offset: 0,
@@ -806,10 +845,10 @@ let main_obj = {
 			csa_count: 0,
 		},
 		call: ex_wrap(function(req) {
-			if (req.args.up == null || !req.args.phy)
+			let phy = phy_name(req.args.phy, req.args.radio);
+			if (req.args.up == null || !phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			let phy = req.args.phy;
 			let config = hostapd.data.config[phy];
 			if (!config || !config.bss || !config.bss[0] || !config.bss[0].ifname)
 				return 0;
@@ -845,10 +884,11 @@ let main_obj = {
 	},
 	config_get_macaddr_list: {
 		args: {
-			phy: ""
+			phy: "",
+			radio: 0,
 		},
 		call: ex_wrap(function(req) {
-			let phy = req.args.phy;
+			let phy = phy_name(req.args.phy, req.args.radio);
 			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
@@ -867,31 +907,34 @@ let main_obj = {
 	config_set: {
 		args: {
 			phy: "",
+			radio: 0,
 			config: "",
 			prev_config: "",
 		},
 		call: ex_wrap(function(req) {
 			let phy = req.args.phy;
+			let radio = req.args.radio;
+			let name = phy_name(phy, radio);
 			let file = req.args.config;
 			let prev_file = req.args.prev_config;
 
 			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			if (prev_file && !hostapd.data.config[phy]) {
-				let config = iface_load_config(prev_file);
+			if (prev_file && !hostapd.data.config[name]) {
+				let config = iface_load_config(phy, radio, prev_file);
 				if (config)
 					config.radio.data = [];
-				hostapd.data.config[phy] = config;
+				hostapd.data.config[name] = config;
 			}
 
-			let config = iface_load_config(file);
+			let config = iface_load_config(phy, radio, file);
 
-			hostapd.printf(`Set new config for phy ${phy}: ${file}`);
-			iface_set_config(phy, config);
+			hostapd.printf(`Set new config for phy ${name}: ${file}`);
+			iface_set_config(name, config);
 
 			if (hostapd.data.auth_obj)
-				hostapd.data.auth_obj.notify("reload", { phy });
+				hostapd.data.auth_obj.notify("reload", { phy, radio });
 
 			return {
 				pid: hostapd.getpid()
@@ -975,10 +1018,18 @@ function bss_event(type, name, data) {
 return {
 	shutdown: function() {
 		for (let phy in hostapd.data.config)
-			iface_set_config(phy, null);
+			iface_set_config(phy);
 		hostapd.udebug_set(null);
 		hostapd.ubus.disconnect();
 	},
+	bss_create: function(phy, name, obj) {
+		phy = hostapd.data.config[phy];
+		if (!phy)
+			return;
+
+		if (phy.radio_idx != null && phy.radio_idx >= 0)
+			wdev_set_radio_mask(name, 1 << phy.radio_idx);
+	},
 	bss_add: function(phy, name, obj) {
 		bss_event("add", name);
 	},
diff --git a/package/network/services/hostapd/files/wpa_supplicant.uc b/package/network/services/hostapd/files/wpa_supplicant.uc
index eed34f8513..fbea27628e 100644
--- a/package/network/services/hostapd/files/wpa_supplicant.uc
+++ b/package/network/services/hostapd/files/wpa_supplicant.uc
@@ -37,7 +37,7 @@ function iface_start(phydev, iface, macaddr_list)
 
 	wpas.data.iface_phy[ifname] = phy;
 	wdev_remove(ifname);
-	let ret = wdev_create(phy, ifname, wdev_config);
+	let ret = phydev.wdev_add(ifname, wdev_config);
 	if (ret)
 		wpas.printf(`Failed to create device ${ifname}: ${ret}`);
 	wdev_set_up(ifname, true);
@@ -61,22 +61,27 @@ function iface_cb(new_if, old_if)
 		iface_stop(old_if);
 }
 
-function prepare_config(config)
+function prepare_config(config, radio)
 {
 	config.config_data = readfile(config.config);
 
-	return { config: config };
+	return { config };
 }
 
-function set_config(phy_name, num_global_macaddr, config_list)
+function set_config(config_name, phy_name, radio, num_global_macaddr, config_list)
 {
-	let phy = wpas.data.config[phy_name];
+	let phy = wpas.data.config[config_name];
+
+	if (radio < 0)
+		radio = null;
 
 	if (!phy) {
 		phy = vlist_new(iface_cb, false);
-		wpas.data.config[phy_name] = phy;
+		phy.name = phy_name;
+		wpas.data.config[config_name] = phy;
 	}
 
+	phy.radio = radio;
 	phy.num_global_macaddr = num_global_macaddr;
 
 	let values = [];
@@ -94,7 +99,7 @@ function start_pending(phy_name)
 	if (!phy || !phy.data)
 		return;
 
-	let phydev = phy_open(phy_name);
+	let phydev = phy_open(phy.name, phy.radio);
 	if (!phydev) {
 		wpas.printf(`Could not open phy ${phy_name}`);
 		return;
@@ -107,17 +112,30 @@ function start_pending(phy_name)
 		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: {
 			phy: "",
+			radio: 0,
 			stop: true,
 		},
 		call: function(req) {
-			if (!req.args.phy || req.args.stop == null)
+			let name = phy_name(req.args.phy, req.args.radio);
+			if (!name || req.args.stop == null)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			let phy = wpas.data.config[req.args.phy];
+			let phy = wpas.data.config[name];
 			if (!phy)
 				return libubus.STATUS_NOT_FOUND;
 
@@ -126,7 +144,7 @@ let main_obj = {
 					for (let ifname in phy.data)
 						iface_stop(phy.data[ifname]);
 				} else {
-					start_pending(req.args.phy);
+					start_pending(name);
 				}
 			} catch (e) {
 				wpas.printf(`Error chaging state: ${e}\n${e.stacktrace[0].context}`);
@@ -138,10 +156,11 @@ let main_obj = {
 	phy_set_macaddr_list: {
 		args: {
 			phy: "",
+			radio: 0,
 			macaddr: [],
 		},
 		call: function(req) {
-			let phy = req.args.phy;
+			let phy = phy_name(req.args.phy, req.args.radio);
 			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
@@ -151,13 +170,15 @@ let main_obj = {
 	},
 	phy_status: {
 		args: {
-			phy: ""
+			phy: "",
+			radio: 0,
 		},
 		call: function(req) {
-			if (!req.args.phy)
+			let phy = phy_name(req.args.phy, req.args.radio);
+			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			let phy = wpas.data.config[req.args.phy];
+			phy = wpas.data.config[phy];
 			if (!phy)
 				return libubus.STATUS_NOT_FOUND;
 
@@ -187,21 +208,23 @@ let main_obj = {
 	config_set: {
 		args: {
 			phy: "",
+			radio: 0,
 			num_global_macaddr: 0,
 			config: [],
 			defer: true,
 		},
 		call: function(req) {
-			if (!req.args.phy)
+			let phy = phy_name(req.args.phy, req.args.radio);
+			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			wpas.printf(`Set new config for phy ${req.args.phy}`);
+			wpas.printf(`Set new config for phy ${phy}`);
 			try {
 				if (req.args.config)
-					set_config(req.args.phy, req.args.num_global_macaddr, req.args.config);
+					set_config(phy, req.args.phy, req.args.radio, req.args.num_global_macaddr, req.args.config);
 
 				if (!req.args.defer)
-					start_pending(req.args.phy);
+					start_pending(phy);
 			} catch (e) {
 				wpas.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
 				return libubus.STATUS_INVALID_ARGUMENT;




More information about the lede-commits mailing list