aboutsummaryrefslogtreecommitdiffstats
path: root/package/network/services/hostapd/files
diff options
context:
space:
mode:
authorFelix Fietkau <nbd@nbd.name>2023-08-29 14:32:42 +0200
committerFelix Fietkau <nbd@nbd.name>2023-09-18 16:52:25 +0200
commit9720b094aef89802327683f25824820581fed0b9 (patch)
tree1add9555f456586bf676dca47ac674887d6f7cc2 /package/network/services/hostapd/files
parent263583dc1e569fae3f8e99d73f2fd72376421b17 (diff)
downloadupstream-9720b094aef89802327683f25824820581fed0b9.tar.gz
upstream-9720b094aef89802327683f25824820581fed0b9.tar.bz2
upstream-9720b094aef89802327683f25824820581fed0b9.zip
hostapd: backport from master, including ucode based reload support
This significantly improves config reload behavior and also fixes some corner cases related to running AP + mesh interfaces at the same time. Signed-off-by: Felix Fietkau <nbd@nbd.name>
Diffstat (limited to 'package/network/services/hostapd/files')
-rw-r--r--package/network/services/hostapd/files/common.uc318
-rw-r--r--package/network/services/hostapd/files/hostapd.sh38
-rw-r--r--package/network/services/hostapd/files/hostapd.uc809
-rw-r--r--package/network/services/hostapd/files/radius.clients1
-rw-r--r--package/network/services/hostapd/files/radius.config9
-rw-r--r--package/network/services/hostapd/files/radius.init42
-rw-r--r--package/network/services/hostapd/files/radius.users14
-rw-r--r--package/network/services/hostapd/files/wdev.uc207
-rw-r--r--package/network/services/hostapd/files/wpa_supplicant-basic.config2
-rw-r--r--package/network/services/hostapd/files/wpa_supplicant-full.config2
-rw-r--r--package/network/services/hostapd/files/wpa_supplicant-mini.config2
-rw-r--r--package/network/services/hostapd/files/wpa_supplicant-p2p.config2
-rw-r--r--package/network/services/hostapd/files/wpa_supplicant.uc330
-rw-r--r--package/network/services/hostapd/files/wpad_acl.json6
14 files changed, 1748 insertions, 34 deletions
diff --git a/package/network/services/hostapd/files/common.uc b/package/network/services/hostapd/files/common.uc
new file mode 100644
index 0000000000..ccffe3eb43
--- /dev/null
+++ b/package/network/services/hostapd/files/common.uc
@@ -0,0 +1,318 @@
+import * as nl80211 from "nl80211";
+import * as rtnl from "rtnl";
+import { readfile, glob, basename, readlink } from "fs";
+
+const iftypes = {
+ ap: nl80211.const.NL80211_IFTYPE_AP,
+ mesh: nl80211.const.NL80211_IFTYPE_MESH_POINT,
+ sta: nl80211.const.NL80211_IFTYPE_STATION,
+ adhoc: nl80211.const.NL80211_IFTYPE_ADHOC,
+ monitor: nl80211.const.NL80211_IFTYPE_MONITOR,
+};
+
+function wdev_remove(name)
+{
+ nl80211.request(nl80211.const.NL80211_CMD_DEL_INTERFACE, 0, { dev: name });
+}
+
+function __phy_is_fullmac(phyidx)
+{
+ let data = nl80211.request(nl80211.const.NL80211_CMD_GET_WIPHY, 0, { wiphy: phyidx });
+
+ return !data.software_iftypes.ap_vlan;
+}
+
+function phy_is_fullmac(phy)
+{
+ let phyidx = int(trim(readfile(`/sys/class/ieee80211/${phy}/index`)));
+
+ return __phy_is_fullmac(phyidx);
+}
+
+function find_reusable_wdev(phyidx)
+{
+ if (!__phy_is_fullmac(phyidx))
+ return null;
+
+ let data = nl80211.request(
+ nl80211.const.NL80211_CMD_GET_INTERFACE,
+ nl80211.const.NLM_F_DUMP,
+ { wiphy: phyidx });
+ for (let res in data)
+ if (trim(readfile(`/sys/class/net/${res.ifname}/operstate`)) == "down")
+ return res.ifname;
+ return null;
+}
+
+function wdev_create(phy, name, data)
+{
+ let phyidx = int(readfile(`/sys/class/ieee80211/${phy}/index`));
+
+ wdev_remove(name);
+
+ if (!iftypes[data.mode])
+ return `Invalid mode: ${data.mode}`;
+
+ let req = {
+ wiphy: phyidx,
+ ifname: name,
+ iftype: iftypes[data.mode],
+ };
+
+ if (data["4addr"])
+ req["4addr"] = data["4addr"];
+ if (data.macaddr)
+ req.mac = data.macaddr;
+
+ 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
+ nl80211.request(
+ nl80211.const.NL80211_CMD_NEW_INTERFACE,
+ nl80211.const.NLM_F_CREATE,
+ req);
+
+ let error = nl80211.error();
+ if (error)
+ return error;
+
+ if (data.powersave != null) {
+ nl80211.request(nl80211.const.NL80211_CMD_SET_POWER_SAVE, 0,
+ { dev: name, ps_state: data.powersave ? 1 : 0});
+ }
+
+ return null;
+}
+
+function phy_sysfs_file(phy, name)
+{
+ return trim(readfile(`/sys/class/ieee80211/${phy}/${name}`));
+}
+
+function macaddr_split(str)
+{
+ return map(split(str, ":"), (val) => hex(val));
+}
+
+function macaddr_join(addr)
+{
+ return join(":", map(addr, (val) => sprintf("%02x", val)));
+}
+
+function wdev_macaddr(wdev)
+{
+ return trim(readfile(`/sys/class/net/${wdev}/address`));
+}
+
+const phy_proto = {
+ macaddr_init: function(used, options) {
+ this.macaddr_options = options ?? {};
+ this.macaddr_list = {};
+
+ if (type(used) == "object")
+ for (let addr in used)
+ this.macaddr_list[addr] = used[addr];
+ else
+ for (let addr in used)
+ this.macaddr_list[addr] = -1;
+
+ this.for_each_wdev((wdev) => {
+ let macaddr = wdev_macaddr(wdev);
+ this.macaddr_list[macaddr] ??= -1;
+ });
+
+ return this.macaddr_list;
+ },
+
+ macaddr_generate: function(data) {
+ let phy = this.name;
+ let idx = int(data.id ?? 0);
+ let mbssid = int(data.mbssid ?? 0) > 0;
+ let num_global = int(data.num_global ?? 1);
+ let use_global = !mbssid && idx < num_global;
+
+ let base_addr = phy_sysfs_file(phy, "macaddress");
+ 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) {
+ let addrs = split(phy_sysfs_file(phy, "addresses"), "\n");
+
+ if (idx < length(addrs))
+ return addrs[idx];
+
+ base_mask = "ff:ff:ff:ff:ff:ff";
+ }
+
+ let addr = macaddr_split(base_addr);
+ let mask = macaddr_split(base_mask);
+ let type;
+
+ if (mbssid)
+ type = "b5";
+ else if (use_global)
+ type = "add";
+ else if (mask[0] > 0)
+ type = "b1";
+ else if (mask[5] < 0xff)
+ type = "b5";
+ else
+ type = "add";
+
+ switch (type) {
+ case "b1":
+ if (!(addr[0] & 2))
+ idx--;
+ addr[0] |= 2;
+ addr[0] ^= idx << 2;
+ break;
+ case "b5":
+ if (mbssid)
+ addr[0] |= 2;
+ addr[5] ^= idx;
+ break;
+ default:
+ for (let i = 5; i > 0; i--) {
+ addr[i] += idx;
+ if (addr[i] < 256)
+ break;
+ addr[i] %= 256;
+ }
+ break;
+ }
+
+ return macaddr_join(addr);
+ },
+
+ macaddr_next: function(val) {
+ let data = this.macaddr_options ?? {};
+ let list = this.macaddr_list;
+
+ for (let i = 0; i < 32; i++) {
+ data.id = i;
+
+ let mac = this.macaddr_generate(data);
+ if (!mac)
+ return null;
+
+ if (list[mac] != null)
+ continue;
+
+ list[mac] = val != null ? val : -1;
+ return mac;
+ }
+ },
+
+ for_each_wdev: function(cb) {
+ let wdevs = glob(`/sys/class/ieee80211/${this.name}/device/net/*`);
+ wdevs = map(wdevs, (arg) => basename(arg));
+ for (let wdev in wdevs) {
+ if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != this.name)
+ continue;
+
+ cb(wdev);
+ }
+ }
+};
+
+function phy_open(phy)
+{
+ let phyidx = readfile(`/sys/class/ieee80211/${phy}/index`);
+ if (!phyidx)
+ return null;
+
+ return proto({
+ name: phy,
+ idx: int(phyidx)
+ }, phy_proto);
+}
+
+const vlist_proto = {
+ update: function(values, arg) {
+ let data = this.data;
+ let cb = this.cb;
+ let seq = { };
+ let new_data = {};
+ let old_data = {};
+
+ this.data = new_data;
+
+ if (type(values) == "object") {
+ for (let key in values) {
+ old_data[key] = data[key];
+ new_data[key] = values[key];
+ delete data[key];
+ }
+ } else {
+ for (let val in values) {
+ let cur_key = val[0];
+ let cur_obj = val[1];
+
+ old_data[cur_key] = data[cur_key];
+ new_data[cur_key] = val[1];
+ delete data[cur_key];
+ }
+ }
+
+ for (let key in data) {
+ cb(null, data[key], arg);
+ delete data[key];
+ }
+ for (let key in new_data)
+ cb(new_data[key], old_data[key], arg);
+ }
+};
+
+function is_equal(val1, val2) {
+ let t1 = type(val1);
+
+ if (t1 != type(val2))
+ return false;
+
+ if (t1 == "array") {
+ if (length(val1) != length(val2))
+ return false;
+
+ for (let i = 0; i < length(val1); i++)
+ if (!is_equal(val1[i], val2[i]))
+ return false;
+
+ return true;
+ } else if (t1 == "object") {
+ for (let key in val1)
+ if (!is_equal(val1[key], val2[key]))
+ return false;
+ for (let key in val2)
+ if (val1[key] == null)
+ return false;
+ return true;
+ } else {
+ return val1 == val2;
+ }
+}
+
+function vlist_new(cb) {
+ return proto({
+ cb: cb,
+ data: {}
+ }, vlist_proto);
+}
+
+export { wdev_remove, wdev_create, is_equal, vlist_new, phy_is_fullmac, phy_open };
diff --git a/package/network/services/hostapd/files/hostapd.sh b/package/network/services/hostapd/files/hostapd.sh
index 28bd210623..c6ae6fb98b 100644
--- a/package/network/services/hostapd/files/hostapd.sh
+++ b/package/network/services/hostapd/files/hostapd.sh
@@ -121,6 +121,7 @@ hostapd_common_add_device_config() {
config_add_array hostapd_options
config_add_int airtime_mode
+ config_add_int mbssid
hostapd_add_log_config
}
@@ -133,7 +134,8 @@ hostapd_prepare_device_config() {
json_get_vars country country3 country_ie beacon_int:100 doth require_mode legacy_rates \
acs_chan_bias local_pwr_constraint spectrum_mgmt_required airtime_mode cell_density \
- rts_threshold beacon_rate rssi_reject_assoc_rssi rssi_ignore_probe_request maxassoc
+ rts_threshold beacon_rate rssi_reject_assoc_rssi rssi_ignore_probe_request maxassoc \
+ mbssid:0
hostapd_set_log_options base_cfg
@@ -234,6 +236,7 @@ hostapd_prepare_device_config() {
[ -n "$rts_threshold" ] && append base_cfg "rts_threshold=$rts_threshold" "$N"
[ "$airtime_mode" -gt 0 ] && append base_cfg "airtime_mode=$airtime_mode" "$N"
[ -n "$maxassoc" ] && append base_cfg "iface_max_num_sta=$maxassoc" "$N"
+ [ "$mbssid" -gt 0 ] && [ "$mbssid" -le 2 ] && append base_cfg "mbssid=$mbssid" "$N"
json_get_values opts hostapd_options
for val in $opts; do
@@ -625,8 +628,7 @@ hostapd_set_bss_options() {
[ -n "$wpa_strict_rekey" ] && append bss_conf "wpa_strict_rekey=$wpa_strict_rekey" "$N"
}
- set_default nasid "${macaddr//\:}"
- append bss_conf "nas_identifier=$nasid" "$N"
+ [ -n "$nasid" ] && append bss_conf "nas_identifier=$nasid" "$N"
[ -n "$acct_interval" ] && \
append bss_conf "radius_acct_interim_interval=$acct_interval" "$N"
@@ -864,8 +866,9 @@ hostapd_set_bss_options() {
[ "$bss_transition" -eq "1" ] && append bss_conf "bss_transition=1" "$N"
[ "$mbo" -eq 1 ] && append bss_conf "mbo=1" "$N"
- json_get_vars ieee80211k rrm_neighbor_report rrm_beacon_report
+ json_get_vars ieee80211k rrm_neighbor_report rrm_beacon_report rnr
set_default ieee80211k 0
+ set_default rnr 0
if [ "$ieee80211k" -eq "1" ]; then
set_default rrm_neighbor_report 1
set_default rrm_beacon_report 1
@@ -876,6 +879,7 @@ hostapd_set_bss_options() {
[ "$rrm_neighbor_report" -eq "1" ] && append bss_conf "rrm_neighbor_report=1" "$N"
[ "$rrm_beacon_report" -eq "1" ] && append bss_conf "rrm_beacon_report=1" "$N"
+ [ "$rnr" -eq "1" ] && append bss_conf "rnr=1" "$N"
json_get_vars ftm_responder stationary_ap lci civic
set_default ftm_responder 0
@@ -1156,9 +1160,6 @@ hostapd_set_bss_options() {
append bss_conf "$val" "$N"
done
- bss_md5sum="$(echo $bss_conf | md5sum | cut -d" " -f1)"
- append bss_conf "config_id=$bss_md5sum" "$N"
-
append "$var" "$bss_conf" "$N"
return 0
}
@@ -1588,29 +1589,6 @@ EOF
return 0
}
-wpa_supplicant_run() {
- local ifname="$1"
- local hostapd_ctrl="$2"
-
- _wpa_supplicant_common "$ifname"
-
- ubus wait_for wpa_supplicant
- local supplicant_res="$(ubus call wpa_supplicant config_add "{ \
- \"driver\": \"${_w_driver:-wext}\", \"ctrl\": \"$_rpath\", \
- \"iface\": \"$ifname\", \"config\": \"$_config\" \
- ${network_bridge:+, \"bridge\": \"$network_bridge\"} \
- ${hostapd_ctrl:+, \"hostapd_ctrl\": \"$hostapd_ctrl\"} \
- }")"
-
- ret="$?"
-
- [ "$ret" != 0 -o -z "$supplicant_res" ] && wireless_setup_vif_failed WPA_SUPPLICANT_FAILED
-
- wireless_add_process "$(jsonfilter -s "$supplicant_res" -l 1 -e @.pid)" "/usr/sbin/wpa_supplicant" 1 1
-
- return $ret
-}
-
hostapd_common_cleanup() {
killall meshd-nl80211
}
diff --git a/package/network/services/hostapd/files/hostapd.uc b/package/network/services/hostapd/files/hostapd.uc
new file mode 100644
index 0000000000..ebf732bea5
--- /dev/null
+++ b/package/network/services/hostapd/files/hostapd.uc
@@ -0,0 +1,809 @@
+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";
+
+let ubus = libubus.connect();
+
+hostapd.data.config = {};
+
+hostapd.data.file_fields = {
+ vlan_file: true,
+ wpa_psk_file: true,
+ accept_mac_file: true,
+ deny_mac_file: true,
+ eap_user_file: true,
+ ca_cert: true,
+ server_cert: true,
+ server_cert2: true,
+ private_key: true,
+ private_key2: true,
+ dh_file: true,
+ eap_sim_db: true,
+};
+
+function iface_remove(cfg)
+{
+ if (!cfg || !cfg.bss || !cfg.bss[0] || !cfg.bss[0].ifname)
+ return;
+
+ hostapd.remove_iface(cfg.bss[0].ifname);
+ for (let bss in cfg.bss)
+ wdev_remove(bss.ifname);
+}
+
+function iface_gen_config(phy, config, start_disabled)
+{
+ let str = `data:
+${join("\n", config.radio.data)}
+channel=${config.radio.channel}
+`;
+
+ for (let i = 0; i < length(config.bss); i++) {
+ let bss = config.bss[i];
+ let type = i > 0 ? "bss" : "interface";
+ let nasid = bss.nasid ?? replace(bss.bssid, ":", "");
+
+ str += `
+${type}=${bss.ifname}
+bssid=${bss.bssid}
+${join("\n", bss.data)}
+nas_identifier=${nasid}
+`;
+ if (start_disabled)
+ str += `
+start_disabled=1
+`;
+ }
+
+ return str;
+}
+
+function iface_freq_info(iface, config, params)
+{
+ let freq = params.frequency;
+ if (!freq)
+ return null;
+
+ let sec_offset = params.sec_chan_offset;
+ if (sec_offset != -1 && sec_offset != 1)
+ sec_offset = 0;
+
+ let width = 0;
+ for (let line in config.radio.data) {
+ if (!sec_offset && match(line, /^ht_capab=.*HT40/)) {
+ sec_offset = null; // auto-detect
+ continue;
+ }
+
+ let val = match(line, /^(vht_oper_chwidth|he_oper_chwidth)=(\d+)/);
+ if (!val)
+ continue;
+
+ val = int(val[2]);
+ if (val > width)
+ width = val;
+ }
+
+ if (freq < 4000)
+ width = 0;
+
+ return hostapd.freq_info(freq, sec_offset, width);
+}
+
+function iface_add(phy, config, phy_status)
+{
+ let config_inline = iface_gen_config(phy, config, !!phy_status);
+
+ let bss = config.bss[0];
+ let ret = hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`);
+ if (ret < 0)
+ return false;
+
+ if (!phy_status)
+ return true;
+
+ let iface = hostapd.interfaces[bss.ifname];
+ if (!iface)
+ return false;
+
+ let freq_info = iface_freq_info(iface, config, phy_status);
+
+ return iface.start(freq_info) >= 0;
+}
+
+function iface_config_macaddr_list(config)
+{
+ let macaddr_list = {};
+ for (let i = 0; i < length(config.bss); i++) {
+ let bss = config.bss[i];
+ if (!bss.default_macaddr)
+ macaddr_list[bss.bssid] = i;
+ }
+
+ return macaddr_list;
+}
+
+function iface_restart(phydev, config, old_config)
+{
+ let phy = phydev.name;
+
+ iface_remove(old_config);
+ iface_remove(config);
+
+ if (!config.bss || !config.bss[0]) {
+ hostapd.printf(`No bss for phy ${phy}`);
+ return;
+ }
+
+ phydev.macaddr_init(iface_config_macaddr_list(config));
+ for (let i = 0; i < length(config.bss); i++) {
+ let bss = config.bss[i];
+ if (bss.default_macaddr)
+ bss.bssid = phydev.macaddr_next();
+ }
+
+ let bss = config.bss[0];
+ let err = wdev_create(phy, bss.ifname, { mode: "ap" });
+ if (err)
+ hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
+
+ let ubus = hostapd.data.ubus;
+ let phy_status = ubus.call("wpa_supplicant", "phy_status", { phy: phy });
+ if (phy_status && phy_status.state == "COMPLETED") {
+ if (iface_add(phy, config, phy_status))
+ return;
+
+ hostapd.printf(`Failed to bring up phy ${phy} ifname=${bss.ifname} with supplicant provided frequency`);
+ }
+
+ ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
+ if (!iface_add(phy, config))
+ hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
+ ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
+}
+
+function array_to_obj(arr, key, start)
+{
+ let obj = {};
+
+ start ??= 0;
+ for (let i = start; i < length(arr); i++) {
+ let cur = arr[i];
+ obj[cur[key]] = cur;
+ }
+
+ return obj;
+}
+
+function find_array_idx(arr, key, val)
+{
+ for (let i = 0; i < length(arr); i++)
+ if (arr[i][key] == val)
+ return i;
+
+ return -1;
+}
+
+function bss_reload_psk(bss, config, old_config)
+{
+ if (is_equal(old_config.hash.wpa_psk_file, config.hash.wpa_psk_file))
+ return;
+
+ old_config.hash.wpa_psk_file = config.hash.wpa_psk_file;
+ if (!is_equal(old_config, config))
+ return;
+
+ let ret = bss.ctrl("RELOAD_WPA_PSK");
+ ret ??= "failed";
+
+ hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
+}
+
+function remove_file_fields(config)
+{
+ return filter(config, (line) => !hostapd.data.file_fields[split(line, "=")[0]]);
+}
+
+function bss_remove_file_fields(config)
+{
+ let new_cfg = {};
+
+ for (let key in config)
+ new_cfg[key] = config[key];
+ new_cfg.data = remove_file_fields(new_cfg.data);
+ new_cfg.hash = {};
+ for (let key in config.hash)
+ new_cfg.hash[key] = config.hash[key];
+ delete new_cfg.hash.wpa_psk_file;
+ delete new_cfg.hash.vlan_file;
+
+ return new_cfg;
+}
+
+function bss_config_hash(config)
+{
+ return hostapd.sha1(remove_file_fields(config) + "");
+}
+
+function bss_find_existing(config, prev_config, prev_hash)
+{
+ let hash = bss_config_hash(config.data);
+
+ for (let i = 0; i < length(prev_config.bss); i++) {
+ if (!prev_hash[i] || hash != prev_hash[i])
+ continue;
+
+ prev_hash[i] = null;
+ return i;
+ }
+
+ return -1;
+}
+
+function get_config_bss(config, idx)
+{
+ if (!config.bss[idx]) {
+ hostapd.printf(`Invalid bss index ${idx}`);
+ return null;
+ }
+
+ let ifname = config.bss[idx].ifname;
+ if (!ifname)
+ hostapd.printf(`Could not find bss ${config.bss[idx].ifname}`);
+
+ return hostapd.bss[ifname];
+}
+
+function iface_reload_config(phydev, config, old_config)
+{
+ let phy = phydev.name;
+
+ if (!old_config || !is_equal(old_config.radio, config.radio))
+ return false;
+
+ if (is_equal(old_config.bss, config.bss))
+ return true;
+
+ if (!old_config.bss || !old_config.bss[0])
+ return false;
+
+ let iface_name = old_config.bss[0].ifname;
+ let iface = hostapd.interfaces[iface_name];
+ if (!iface) {
+ hostapd.printf(`Could not find previous interface ${iface_name}`);
+ return false;
+ }
+
+ let first_bss = hostapd.bss[iface_name];
+ if (!first_bss) {
+ hostapd.printf(`Could not find bss of previous interface ${iface_name}`);
+ return false;
+ }
+
+ let macaddr_list = iface_config_macaddr_list(config);
+ let bss_list = [];
+ let bss_list_cfg = [];
+ let prev_bss_hash = [];
+
+ for (let bss in old_config.bss) {
+ let hash = bss_config_hash(bss.data);
+ push(prev_bss_hash, bss_config_hash(bss.data));
+ }
+
+ // Step 1: find (possibly renamed) interfaces with the same config
+ // and store them in the new order (with gaps)
+ for (let i = 0; i < length(config.bss); i++) {
+ let prev;
+
+ // For fullmac devices, the first interface needs to be preserved,
+ // since it's treated as the master
+ if (!i && phy_is_fullmac(phy)) {
+ prev = 0;
+ prev_bss_hash[0] = null;
+ } else {
+ prev = bss_find_existing(config.bss[i], old_config, prev_bss_hash);
+ }
+ if (prev < 0)
+ continue;
+
+ let cur_config = config.bss[i];
+ let prev_config = old_config.bss[prev];
+
+ let prev_bss = get_config_bss(old_config, prev);
+ if (!prev_bss)
+ return false;
+
+ // try to preserve MAC address of this BSS by reassigning another
+ // BSS if necessary
+ if (cur_config.default_macaddr &&
+ !macaddr_list[prev_config.bssid]) {
+ macaddr_list[prev_config.bssid] = i;
+ cur_config.bssid = prev_config.bssid;
+ }
+
+ bss_list[i] = prev_bss;
+ bss_list_cfg[i] = old_config.bss[prev];
+ }
+
+ if (config.mbssid && !bss_list_cfg[0]) {
+ hostapd.printf("First BSS changed with MBSSID enabled");
+ return false;
+ }
+
+ // Step 2: if none were found, rename and preserve the first one
+ if (length(bss_list) == 0) {
+ // can't change the bssid of the first bss
+ if (config.bss[0].bssid != old_config.bss[0].bssid) {
+ if (!config.bss[0].default_macaddr) {
+ hostapd.printf(`BSSID of first interface changed: ${lc(old_config.bss[0].bssid)} -> ${lc(config.bss[0].bssid)}`);
+ return false;
+ }
+
+ config.bss[0].bssid = old_config.bss[0].bssid;
+ }
+
+ let prev_bss = get_config_bss(old_config, 0);
+ if (!prev_bss)
+ return false;
+
+ macaddr_list[config.bss[0].bssid] = 0;
+ bss_list[0] = prev_bss;
+ bss_list_cfg[0] = old_config.bss[0];
+ prev_bss_hash[0] = null;
+ }
+
+ // Step 3: delete all unused old interfaces
+ for (let i = 0; i < length(prev_bss_hash); i++) {
+ if (!prev_bss_hash[i])
+ continue;
+
+ let prev_bss = get_config_bss(old_config, i);
+ if (!prev_bss)
+ return false;
+
+ let ifname = old_config.bss[i].ifname;
+ hostapd.printf(`Remove bss '${ifname}' on phy '${phy}'`);
+ prev_bss.delete();
+ wdev_remove(ifname);
+ }
+
+ // Step 4: rename preserved interfaces, use temporary name on duplicates
+ let rename_list = [];
+ for (let i = 0; i < length(bss_list); i++) {
+ if (!bss_list[i])
+ continue;
+
+ let old_ifname = bss_list_cfg[i].ifname;
+ let new_ifname = config.bss[i].ifname;
+ if (old_ifname == new_ifname)
+ continue;
+
+ if (hostapd.bss[new_ifname]) {
+ new_ifname = "tmp_" + substr(hostapd.sha1(new_ifname), 0, 8);
+ push(rename_list, i);
+ }
+
+ hostapd.printf(`Rename bss ${old_ifname} to ${new_ifname}`);
+ if (!bss_list[i].rename(new_ifname)) {
+ hostapd.printf(`Failed to rename bss ${old_ifname} to ${new_ifname}`);
+ return false;
+ }
+
+ bss_list_cfg[i].ifname = new_ifname;
+ }
+
+ // Step 5: rename interfaces with temporary names
+ for (let i in rename_list) {
+ let new_ifname = config.bss[i].ifname;
+ if (!bss_list[i].rename(new_ifname)) {
+ hostapd.printf(`Failed to rename bss to ${new_ifname}`);
+ return false;
+ }
+ bss_list_cfg[i].ifname = new_ifname;
+ }
+
+ // Step 6: assign BSSID for newly created interfaces
+ let macaddr_data = {
+ num_global: config.num_global_macaddr ?? 1,
+ mbssid: config.mbssid ?? 0,
+ };
+ macaddr_list = phydev.macaddr_init(macaddr_list, macaddr_data);
+ for (let i = 0; i < length(config.bss); i++) {
+ if (bss_list[i])
+ continue;
+ let bsscfg = config.bss[i];
+
+ let mac_idx = macaddr_list[bsscfg.bssid];
+ if (mac_idx < 0)
+ macaddr_list[bsscfg.bssid] = i;
+ if (mac_idx == i)
+ continue;
+
+ // statically assigned bssid of the new interface is in conflict
+ // with the bssid of a reused interface. reassign the reused interface
+ if (!bsscfg.default_macaddr) {
+ // can't update bssid of the first BSS, need to restart
+ if (!mac_idx < 0)
+ return false;
+
+ bsscfg = config.bss[mac_idx];
+ }
+
+ let addr = phydev.macaddr_next(i);
+ if (!addr) {
+ hostapd.printf(`Failed to generate mac address for phy ${phy}`);
+ return false;
+ }
+ bsscfg.bssid = addr;
+ }
+
+ let config_inline = iface_gen_config(phy, config);
+
+ // Step 7: fill in the gaps with new interfaces
+ for (let i = 0; i < length(config.bss); i++) {
+ let ifname = config.bss[i].ifname;
+ let bss = bss_list[i];
+
+ if (bss)
+ continue;
+
+ hostapd.printf(`Add bss ${ifname} on phy ${phy}`);
+ bss_list[i] = iface.add_bss(config_inline, i);
+ if (!bss_list[i]) {
+ hostapd.printf(`Failed to add new bss ${ifname} on phy ${phy}`);
+ 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}'`);
+ return false;
+ }
+
+ // Step 9: update config
+ for (let i = 0; i < length(config.bss); i++) {
+ if (!bss_list_cfg[i])
+ continue;
+
+ let ifname = config.bss[i].ifname;
+ let bss = bss_list[i];
+
+ if (is_equal(config.bss[i], bss_list_cfg[i]))
+ continue;
+
+ if (is_equal(bss_remove_file_fields(config.bss[i]),
+ bss_remove_file_fields(bss_list_cfg[i]))) {
+ hostapd.printf(`Update config data files for bss ${ifname}`);
+ if (bss.set_config(config_inline, i, true) < 0) {
+ hostapd.printf(`Could not update config data files for bss ${ifname}`);
+ return false;
+ } else {
+ bss.ctrl("RELOAD_WPA_PSK");
+ continue;
+ }
+ }
+
+ bss_reload_psk(bss, config.bss[i], bss_list_cfg[i]);
+ if (is_equal(config.bss[i], bss_list_cfg[i]))
+ continue;
+
+ hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
+ if (bss.set_config(config_inline, i) < 0) {
+ hostapd.printf(`Failed to set config for bss ${ifname}`);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function iface_update_supplicant_macaddr(phy, config)
+{
+ let macaddr_list = [];
+ for (let i = 0; i < length(config.bss); i++)
+ push(macaddr_list, config.bss[i].bssid);
+ ubus.call("wpa_supplicant", "phy_set_macaddr_list", { phy: phy, macaddr: macaddr_list });
+}
+
+function iface_set_config(phy, config)
+{
+ let old_config = hostapd.data.config[phy];
+
+ hostapd.data.config[phy] = config;
+
+ if (!config)
+ return iface_remove(old_config);
+
+ let phydev = phy_open(phy);
+ if (!phydev) {
+ hostapd.printf(`Failed to open phy ${phy}`);
+ return false;
+ }
+
+ try {
+ let ret = iface_reload_config(phydev, config, old_config);
+ if (ret) {
+ iface_update_supplicant_macaddr(phy, config);
+ hostapd.printf(`Reloaded settings for phy ${phy}`);
+ return 0;
+ }
+ } catch (e) {
+ hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
+ }
+
+ hostapd.printf(`Restart interface for phy ${phy}`);
+ let ret = iface_restart(phydev, config, old_config);
+ iface_update_supplicant_macaddr(phy, config);
+
+ return ret;
+}
+
+function config_add_bss(config, name)
+{
+ let bss = {
+ ifname: name,
+ data: [],
+ hash: {}
+ };
+
+ push(config.bss, bss);
+
+ return bss;
+}
+
+function iface_load_config(filename)
+{
+ let f = open(filename, "r");
+ if (!f)
+ return null;
+
+ let config = {
+ radio: {
+ data: []
+ },
+ bss: [],
+ orig_file: filename,
+ };
+
+ let bss;
+ let line;
+ while ((line = trim(f.read("line"))) != null) {
+ let val = split(line, "=", 2);
+ if (!val[0])
+ continue;
+
+ if (val[0] == "interface") {
+ bss = config_add_bss(config, val[1]);
+ break;
+ }
+
+ if (val[0] == "channel") {
+ config.radio.channel = val[1];
+ continue;
+ }
+
+ if (val[0] == "#num_global_macaddr" ||
+ val[0] == "mbssid")
+ config[val[0]] = int(val[1]);
+
+ push(config.radio.data, line);
+ }
+
+ while ((line = trim(f.read("line"))) != null) {
+ if (line == "#default_macaddr")
+ bss.default_macaddr = true;
+
+ let val = split(line, "=", 2);
+ if (!val[0])
+ continue;
+
+ if (val[0] == "bssid") {
+ bss.bssid = lc(val[1]);
+ continue;
+ }
+
+ if (val[0] == "nas_identifier")
+ bss.nasid = val[1];
+
+ if (val[0] == "bss") {
+ bss = config_add_bss(config, val[1]);
+ continue;
+ }
+
+ if (hostapd.data.file_fields[val[0]])
+ bss.hash[val[0]] = hostapd.sha1(readfile(val[1]));
+
+ push(bss.data, line);
+ }
+ f.close();
+
+ return config;
+}
+
+function ex_wrap(func) {
+ return (req) => {
+ try {
+ let ret = func(req);
+ return ret;
+ } catch(e) {
+ hostapd.printf(`Exception in ubus function: ${e}\n${e.stacktrace[0].context}`);
+ }
+ return libubus.STATUS_UNKNOWN_ERROR;
+ };
+}
+
+let main_obj = {
+ reload: {
+ args: {
+ phy: "",
+ },
+ call: ex_wrap(function(req) {
+ let phy_list = req.args.phy ? [ req.args.phy ] : 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);
+ iface_set_config(phy_name, config);
+ }
+
+ return 0;
+ })
+ },
+ apsta_state: {
+ args: {
+ phy: "",
+ up: true,
+ frequency: 0,
+ sec_chan_offset: 0,
+ csa: true,
+ csa_count: 0,
+ },
+ call: ex_wrap(function(req) {
+ if (req.args.up == null || !req.args.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;
+
+ let iface = hostapd.interfaces[config.bss[0].ifname];
+ if (!iface)
+ return 0;
+
+ if (!req.args.up) {
+ iface.stop();
+ return 0;
+ }
+
+ if (!req.args.frequency)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ let freq_info = iface_freq_info(iface, config, req.args);
+ if (!freq_info)
+ return libubus.STATUS_UNKNOWN_ERROR;
+
+ let ret;
+ if (req.args.csa) {
+ freq_info.csa_count = req.args.csa_count ?? 10;
+ ret = iface.switch_channel(freq_info);
+ } else {
+ ret = iface.start(freq_info);
+ }
+ if (!ret)
+ return libubus.STATUS_UNKNOWN_ERROR;
+
+ return 0;
+ })
+ },
+ config_get_macaddr_list: {
+ args: {
+ phy: ""
+ },
+ call: ex_wrap(function(req) {
+ let phy = req.args.phy;
+ if (!phy)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ let ret = {
+ macaddr: [],
+ };
+
+ let config = hostapd.data.config[phy];
+ if (!config)
+ return ret;
+
+ ret.macaddr = map(config.bss, (bss) => bss.bssid);
+ return ret;
+ })
+ },
+ config_set: {
+ args: {
+ phy: "",
+ config: "",
+ prev_config: "",
+ },
+ call: ex_wrap(function(req) {
+ let phy = req.args.phy;
+ 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 (config)
+ config.radio.data = [];
+ hostapd.data.config[phy] = config;
+ }
+
+ let config = iface_load_config(file);
+
+ hostapd.printf(`Set new config for phy ${phy}: ${file}`);
+ iface_set_config(phy, config);
+
+ return {
+ pid: hostapd.getpid()
+ };
+ })
+ },
+ config_add: {
+ args: {
+ iface: "",
+ config: "",
+ },
+ call: ex_wrap(function(req) {
+ if (!req.args.iface || !req.args.config)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ if (hostapd.add_iface(`bss_config=${req.args.iface}:${req.args.config}`) < 0)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ return {
+ pid: hostapd.getpid()
+ };
+ })
+ },
+ config_remove: {
+ args: {
+ iface: ""
+ },
+ call: ex_wrap(function(req) {
+ if (!req.args.iface)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ hostapd.remove_iface(req.args.iface);
+ return 0;
+ })
+ },
+};
+
+hostapd.data.ubus = ubus;
+hostapd.data.obj = ubus.publish("hostapd", main_obj);
+
+function bss_event(type, name, data) {
+ let ubus = hostapd.data.ubus;
+
+ data ??= {};
+ data.name = name;
+ hostapd.data.obj.notify(`bss.${type}`, data, null, null, null, -1);
+ ubus.call("service", "event", { type: `hostapd.${name}.${type}`, data: {} });
+}
+
+return {
+ shutdown: function() {
+ for (let phy in hostapd.data.config)
+ iface_set_config(phy, null);
+ hostapd.ubus.disconnect();
+ },
+ bss_add: function(name, obj) {
+ bss_event("add", name);
+ },
+ bss_reload: function(name, obj, reconf) {
+ bss_event("reload", name, { reconf: reconf != 0 });
+ },
+ bss_remove: function(name, obj) {
+ bss_event("remove", name);
+ }
+};
diff --git a/package/network/services/hostapd/files/radius.clients b/package/network/services/hostapd/files/radius.clients
new file mode 100644
index 0000000000..3175dcfd04
--- /dev/null
+++ b/package/network/services/hostapd/files/radius.clients
@@ -0,0 +1 @@
+0.0.0.0/0 radius
diff --git a/package/network/services/hostapd/files/radius.config b/package/network/services/hostapd/files/radius.config
new file mode 100644
index 0000000000..ad8730748b
--- /dev/null
+++ b/package/network/services/hostapd/files/radius.config
@@ -0,0 +1,9 @@
+config radius
+ option disabled '1'
+ option ca_cert '/etc/radius/ca.pem'
+ option cert '/etc/radius/cert.pem'
+ option key '/etc/radius/key.pem'
+ option users '/etc/radius/users'
+ option clients '/etc/radius/clients'
+ option auth_port '1812'
+ option acct_port '1813'
diff --git a/package/network/services/hostapd/files/radius.init b/package/network/services/hostapd/files/radius.init
new file mode 100644
index 0000000000..4c562c2473
--- /dev/null
+++ b/package/network/services/hostapd/files/radius.init
@@ -0,0 +1,42 @@
+#!/bin/sh /etc/rc.common
+
+START=30
+
+USE_PROCD=1
+NAME=radius
+
+radius_start() {
+ local cfg="$1"
+
+ config_get_bool disabled "$cfg" disabled 0
+
+ [ "$disabled" -gt 0 ] && return
+
+ config_get ca "$cfg" ca_cert
+ config_get key "$cfg" key
+ config_get cert "$cfg" cert
+ config_get users "$cfg" users
+ config_get clients "$cfg" clients
+ config_get auth_port "$cfg" auth_port 1812
+ config_get acct_port "$cfg" acct_port 1813
+ config_get identity "$cfg" identity "$(cat /proc/sys/kernel/hostname)"
+
+ procd_open_instance $cfg
+ procd_set_param command /usr/sbin/hostapd-radius \
+ -C "$ca" \
+ -c "$cert" -k "$key" \
+ -s "$clients" -u "$users" \
+ -p "$auth_port" -P "$acct_port" \
+ -i "$identity"
+ procd_close_instance
+}
+
+start_service() {
+ config_load radius
+ config_foreach radius_start radius
+}
+
+service_triggers()
+{
+ procd_add_reload_trigger "radius"
+}
diff --git a/package/network/services/hostapd/files/radius.users b/package/network/services/hostapd/files/radius.users
new file mode 100644
index 0000000000..03e2fc8fae
--- /dev/null
+++ b/package/network/services/hostapd/files/radius.users
@@ -0,0 +1,14 @@
+{
+ "phase1": {
+ "wildcard": [
+ {
+ "name": "*",
+ "methods": [ "PEAP" ]
+ }
+ ]
+ },
+ "phase2": {
+ "users": {
+ }
+ }
+}
diff --git a/package/network/services/hostapd/files/wdev.uc b/package/network/services/hostapd/files/wdev.uc
new file mode 100644
index 0000000000..8a031b40b9
--- /dev/null
+++ b/package/network/services/hostapd/files/wdev.uc
@@ -0,0 +1,207 @@
+#!/usr/bin/env ucode
+'use strict';
+import { vlist_new, is_equal, wdev_create, wdev_remove, 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 command = shift(ARGV);
+let phydev;
+
+const mesh_params = [
+ "mesh_retry_timeout", "mesh_confirm_timeout", "mesh_holding_timeout", "mesh_max_peer_links",
+ "mesh_max_retries", "mesh_ttl", "mesh_element_ttl", "mesh_hwmp_max_preq_retries",
+ "mesh_path_refresh_time", "mesh_min_discovery_timeout", "mesh_hwmp_active_path_timeout",
+ "mesh_hwmp_preq_min_interval", "mesh_hwmp_net_diameter_traversal_time", "mesh_hwmp_rootmode",
+ "mesh_hwmp_rann_interval", "mesh_gate_announcements", "mesh_sync_offset_max_neighor",
+ "mesh_rssi_threshold", "mesh_hwmp_active_path_to_root_timeout", "mesh_hwmp_root_interval",
+ "mesh_hwmp_confirmation_interval", "mesh_awake_window", "mesh_plink_timeout",
+ "mesh_auto_open_plinks", "mesh_fwding", "mesh_power_mode"
+];
+
+function iface_stop(wdev)
+{
+ if (keep_devices[wdev.ifname])
+ return;
+
+ wdev_remove(wdev.ifname);
+}
+
+function iface_start(wdev)
+{
+ let ifname = wdev.ifname;
+
+ if (readfile(`/sys/class/net/${ifname}/ifindex`)) {
+ system([ "ip", "link", "set", "dev", ifname, "down" ]);
+ wdev_remove(ifname);
+ }
+ let wdev_config = {};
+ for (let key in wdev)
+ wdev_config[key] = wdev[key];
+ if (!wdev_config.macaddr && wdev.mode != "monitor")
+ wdev_config.macaddr = phydev.macaddr_next();
+ wdev_create(phy, ifname, wdev);
+ system([ "ip", "link", "set", "dev", ifname, "up" ]);
+ if (wdev.freq)
+ system(`iw dev ${ifname} set freq ${wdev.freq} ${wdev.htmode}`);
+ if (wdev.mode == "adhoc") {
+ let cmd = ["iw", "dev", ifname, "ibss", "join", wdev.ssid, wdev.freq, wdev.htmode, "fixed-freq" ];
+ if (wdev.bssid)
+ push(cmd, wdev.bssid);
+ for (let key in [ "beacon-interval", "basic-rates", "mcast-rate", "keys" ])
+ if (wdev[key])
+ push(cmd, key, wdev[key]);
+ system(cmd);
+ } else if (wdev.mode == "mesh") {
+ let cmd = [ "iw", "dev", ifname, "mesh", "join", wdev.ssid, "freq", wdev.freq, wdev.htmode ];
+ for (let key in [ "mcast-rate", "beacon-interval" ])
+ if (wdev[key])
+ push(cmd, key, wdev[key]);
+ system(cmd);
+
+ cmd = ["iw", "dev", ifname, "set", "mesh_param" ];
+ let len = length(cmd);
+
+ for (let param in mesh_params)
+ if (wdev[param])
+ push(cmd, param, wdev[param]);
+
+ if (len == length(cmd))
+ return;
+
+ system(cmd);
+ }
+
+}
+
+function iface_cb(new_if, old_if)
+{
+ if (old_if && new_if && is_equal(old_if, new_if))
+ return;
+
+ if (old_if)
+ iface_stop(old_if);
+ if (new_if)
+ iface_start(new_if);
+}
+
+function drop_inactive(config)
+{
+ for (let key in config) {
+ if (!readfile(`/sys/class/net/${key}/ifindex`))
+ delete config[key];
+ }
+}
+
+function add_ifname(config)
+{
+ for (let key in config)
+ config[key].ifname = key;
+}
+
+function delete_ifname(config)
+{
+ for (let key in config)
+ delete config[key].ifname;
+}
+
+function add_existing(phy, config)
+{
+ let wdevs = glob(`/sys/class/ieee80211/${phy}/device/net/*`);
+ wdevs = map(wdevs, (arg) => basename(arg));
+ for (let wdev in wdevs) {
+ if (config[wdev])
+ continue;
+
+ if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != phy)
+ continue;
+
+ if (trim(readfile(`/sys/class/net/${wdev}/operstate`)) == "down")
+ config[wdev] = {};
+ }
+}
+
+function usage()
+{
+ warn(`Usage: ${basename(sourcepath())} <phy> <command> [<arguments>]
+
+Commands:
+ set_config <config> [<device]...] - set phy configuration
+ get_macaddr <id> - get phy MAC address for vif index <id>
+`);
+ exit(1);
+}
+
+const commands = {
+ set_config: function(args) {
+ let statefile = `/var/run/wdev-${phy}.json`;
+
+ let new_config = shift(args);
+ for (let dev in ARGV)
+ keep_devices[dev] = true;
+
+ if (!new_config)
+ usage();
+
+ new_config = json(new_config);
+ if (!new_config) {
+ warn("Invalid configuration\n");
+ exit(1);
+ }
+
+ let old_config = readfile(statefile);
+ if (old_config)
+ old_config = json(old_config);
+
+ let config = vlist_new(iface_cb);
+ if (type(old_config) == "object")
+ config.data = old_config;
+
+ add_existing(phy, 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 macaddr_list = [];
+ if (type(data) == "object" && data.macaddr)
+ macaddr_list = data.macaddr;
+ ubus.disconnect();
+ phydev.macaddr_init(macaddr_list);
+
+ add_ifname(new_config);
+ config.update(new_config);
+
+ drop_inactive(config.data);
+ delete_ifname(config.data);
+ writefile(statefile, sprintf("%J", config.data));
+ },
+ get_macaddr: function(args) {
+ let data = {};
+
+ for (let arg in args) {
+ arg = split(arg, "=", 2);
+ data[arg[0]] = arg[1];
+ }
+
+ let macaddr = phydev.macaddr_generate(data);
+ if (!macaddr) {
+ warn(`Could not get MAC address for phy ${phy}\n`);
+ exit(1);
+ }
+
+ print(macaddr + "\n");
+ },
+};
+
+if (!phy || !command | !commands[command])
+ usage();
+
+phydev = phy_open(phy);
+if (!phydev) {
+ warn(`PHY ${phy} does not exist\n`);
+ exit(1);
+}
+
+commands[command](ARGV);
diff --git a/package/network/services/hostapd/files/wpa_supplicant-basic.config b/package/network/services/hostapd/files/wpa_supplicant-basic.config
index 6abd8e2331..944b4d9287 100644
--- a/package/network/services/hostapd/files/wpa_supplicant-basic.config
+++ b/package/network/services/hostapd/files/wpa_supplicant-basic.config
@@ -26,7 +26,7 @@
# replacement for WEXT and its use allows wpa_supplicant to properly control
# the driver to improve existing functionality like roaming and to support new
# functionality.
-CONFIG_DRIVER_WEXT=y
+#CONFIG_DRIVER_WEXT=y
# Driver interface for Linux drivers using the nl80211 kernel interface
CONFIG_DRIVER_NL80211=y
diff --git a/package/network/services/hostapd/files/wpa_supplicant-full.config b/package/network/services/hostapd/files/wpa_supplicant-full.config
index d24fbbb01f..b39dabca06 100644
--- a/package/network/services/hostapd/files/wpa_supplicant-full.config
+++ b/package/network/services/hostapd/files/wpa_supplicant-full.config
@@ -26,7 +26,7 @@
# replacement for WEXT and its use allows wpa_supplicant to properly control
# the driver to improve existing functionality like roaming and to support new
# functionality.
-CONFIG_DRIVER_WEXT=y
+#CONFIG_DRIVER_WEXT=y
# Driver interface for Linux drivers using the nl80211 kernel interface
CONFIG_DRIVER_NL80211=y
diff --git a/package/network/services/hostapd/files/wpa_supplicant-mini.config b/package/network/services/hostapd/files/wpa_supplicant-mini.config
index 9eb1111e52..2a3f8fb69d 100644
--- a/package/network/services/hostapd/files/wpa_supplicant-mini.config
+++ b/package/network/services/hostapd/files/wpa_supplicant-mini.config
@@ -26,7 +26,7 @@
# replacement for WEXT and its use allows wpa_supplicant to properly control
# the driver to improve existing functionality like roaming and to support new
# functionality.
-CONFIG_DRIVER_WEXT=y
+#CONFIG_DRIVER_WEXT=y
# Driver interface for Linux drivers using the nl80211 kernel interface
CONFIG_DRIVER_NL80211=y
diff --git a/package/network/services/hostapd/files/wpa_supplicant-p2p.config b/package/network/services/hostapd/files/wpa_supplicant-p2p.config
index 0dcc88e648..7f5140622c 100644
--- a/package/network/services/hostapd/files/wpa_supplicant-p2p.config
+++ b/package/network/services/hostapd/files/wpa_supplicant-p2p.config
@@ -26,7 +26,7 @@
# replacement for WEXT and its use allows wpa_supplicant to properly control
# the driver to improve existing functionality like roaming and to support new
# functionality.
-CONFIG_DRIVER_WEXT=y
+#CONFIG_DRIVER_WEXT=y
# Driver interface for Linux drivers using the nl80211 kernel interface
CONFIG_DRIVER_NL80211=y
diff --git a/package/network/services/hostapd/files/wpa_supplicant.uc b/package/network/services/hostapd/files/wpa_supplicant.uc
new file mode 100644
index 0000000000..cb5f41b1af
--- /dev/null
+++ b/package/network/services/hostapd/files/wpa_supplicant.uc
@@ -0,0 +1,330 @@
+let libubus = require("ubus");
+import { open, readfile } from "fs";
+import { wdev_create, wdev_remove, is_equal, vlist_new, phy_open } from "common";
+
+let ubus = libubus.connect();
+
+wpas.data.config = {};
+wpas.data.iface_phy = {};
+wpas.data.macaddr_list = {};
+
+function iface_stop(iface)
+{
+ let ifname = iface.config.iface;
+
+ if (!iface.running)
+ return;
+
+ delete wpas.data.iface_phy[ifname];
+ wpas.remove_iface(ifname);
+ wdev_remove(ifname);
+ iface.running = false;
+}
+
+function iface_start(phydev, iface, macaddr_list)
+{
+ let phy = phydev.name;
+
+ if (iface.running)
+ return;
+
+ let ifname = iface.config.iface;
+ let wdev_config = {};
+ for (let field in iface.config)
+ wdev_config[field] = iface.config[field];
+ if (!wdev_config.macaddr)
+ wdev_config.macaddr = phydev.macaddr_next();
+
+ wpas.data.iface_phy[ifname] = phy;
+ wdev_remove(ifname);
+ let ret = wdev_create(phy, ifname, wdev_config);
+ if (ret)
+ wpas.printf(`Failed to create device ${ifname}: ${ret}`);
+ wpas.add_iface(iface.config);
+ iface.running = true;
+}
+
+function iface_cb(new_if, old_if)
+{
+ if (old_if && new_if && is_equal(old_if.config, new_if.config)) {
+ new_if.running = old_if.running;
+ return;
+ }
+
+ if (new_if && old_if)
+ wpas.printf(`Update configuration for interface ${old_if.config.iface}`);
+ else if (old_if)
+ wpas.printf(`Remove interface ${old_if.config.iface}`);
+
+ if (old_if)
+ iface_stop(old_if);
+}
+
+function prepare_config(config)
+{
+ config.config_data = readfile(config.config);
+
+ return { config: config };
+}
+
+function set_config(phy_name, config_list)
+{
+ let phy = wpas.data.config[phy_name];
+
+ if (!phy) {
+ phy = vlist_new(iface_cb, false);
+ wpas.data.config[phy_name] = phy;
+ }
+
+ let values = [];
+ for (let config in config_list)
+ push(values, [ config.iface, prepare_config(config) ]);
+
+ 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);
+ 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);
+
+ for (let ifname in phy.data)
+ iface_start(phydev, phy.data[ifname]);
+}
+
+let main_obj = {
+ phy_set_state: {
+ args: {
+ phy: "",
+ stop: true,
+ },
+ call: function(req) {
+ if (!req.args.phy || req.args.stop == null)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ let phy = wpas.data.config[req.args.phy];
+ if (!phy)
+ return libubus.STATUS_NOT_FOUND;
+
+ try {
+ if (req.args.stop) {
+ for (let ifname in phy.data)
+ iface_stop(phy.data[ifname]);
+ } else {
+ start_pending(req.args.phy);
+ }
+ } catch (e) {
+ wpas.printf(`Error chaging state: ${e}\n${e.stacktrace[0].context}`);
+ return libubus.STATUS_INVALID_ARGUMENT;
+ }
+ return 0;
+ }
+ },
+ phy_set_macaddr_list: {
+ args: {
+ phy: "",
+ macaddr: [],
+ },
+ call: function(req) {
+ let phy = req.args.phy;
+ if (!phy)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ wpas.data.macaddr_list[phy] = req.args.macaddr;
+ return 0;
+ }
+ },
+ phy_status: {
+ args: {
+ phy: ""
+ },
+ call: function(req) {
+ if (!req.args.phy)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ let phy = wpas.data.config[req.args.phy];
+ if (!phy)
+ return libubus.STATUS_NOT_FOUND;
+
+ for (let ifname in phy.data) {
+ try {
+ let iface = wpas.interfaces[ifname];
+ if (!iface)
+ continue;
+
+ let status = iface.status();
+ if (!status)
+ continue;
+
+ if (status.state == "INTERFACE_DISABLED")
+ continue;
+
+ status.ifname = ifname;
+ return status;
+ } catch (e) {
+ continue;
+ }
+ }
+
+ return libubus.STATUS_NOT_FOUND;
+ }
+ },
+ config_set: {
+ args: {
+ phy: "",
+ config: [],
+ defer: true,
+ },
+ call: function(req) {
+ if (!req.args.phy)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ wpas.printf(`Set new config for phy ${req.args.phy}`);
+ try {
+ if (req.args.config)
+ set_config(req.args.phy, req.args.config);
+
+ if (!req.args.defer)
+ start_pending(req.args.phy);
+ } catch (e) {
+ wpas.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
+ return libubus.STATUS_INVALID_ARGUMENT;
+ }
+
+ return {
+ pid: wpas.getpid()
+ };
+ }
+ },
+ config_add: {
+ args: {
+ driver: "",
+ iface: "",
+ bridge: "",
+ hostapd_ctrl: "",
+ ctrl: "",
+ config: "",
+ },
+ call: function(req) {
+ if (!req.args.iface || !req.args.config)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ if (wpas.add_iface(req.args) < 0)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ return {
+ pid: wpas.getpid()
+ };
+ }
+ },
+ config_remove: {
+ args: {
+ iface: ""
+ },
+ call: function(req) {
+ if (!req.args.iface)
+ return libubus.STATUS_INVALID_ARGUMENT;
+
+ wpas.remove_iface(req.args.iface);
+ return 0;
+ }
+ },
+};
+
+wpas.data.ubus = ubus;
+wpas.data.obj = ubus.publish("wpa_supplicant", main_obj);
+
+function iface_event(type, name, data) {
+ let ubus = wpas.data.ubus;
+
+ data ??= {};
+ data.name = name;
+ wpas.data.obj.notify(`iface.${type}`, data, null, null, null, -1);
+ ubus.call("service", "event", { type: `wpa_supplicant.${name}.${type}`, data: {} });
+}
+
+function iface_hostapd_notify(phy, ifname, iface, state)
+{
+ let ubus = wpas.data.ubus;
+ let status = iface.status();
+ let msg = { phy: phy };
+
+ switch (state) {
+ case "DISCONNECTED":
+ case "AUTHENTICATING":
+ case "SCANNING":
+ msg.up = false;
+ break;
+ case "INTERFACE_DISABLED":
+ case "INACTIVE":
+ msg.up = true;
+ break;
+ case "COMPLETED":
+ msg.up = true;
+ msg.frequency = status.frequency;
+ msg.sec_chan_offset = status.sec_chan_offset;
+ break;
+ default:
+ return;
+ }
+
+ ubus.call("hostapd", "apsta_state", msg);
+}
+
+function iface_channel_switch(phy, 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,
+ };
+ ubus.call("hostapd", "apsta_state", msg);
+}
+
+return {
+ shutdown: function() {
+ for (let phy in wpas.data.config)
+ set_config(phy, []);
+ wpas.ubus.disconnect();
+ },
+ iface_add: function(name, obj) {
+ iface_event("add", name);
+ },
+ iface_remove: function(name, obj) {
+ 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;
+ }
+
+ iface_hostapd_notify(phy, ifname, iface, state);
+ },
+ 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);
+ }
+};
diff --git a/package/network/services/hostapd/files/wpad_acl.json b/package/network/services/hostapd/files/wpad_acl.json
index c77ccd8ea0..d00fd945ba 100644
--- a/package/network/services/hostapd/files/wpad_acl.json
+++ b/package/network/services/hostapd/files/wpad_acl.json
@@ -3,6 +3,12 @@
"access": {
"service": {
"methods": [ "event" ]
+ },
+ "wpa_supplicant": {
+ "methods": [ "phy_set_state", "phy_set_macaddr_list", "phy_status" ]
+ },
+ "hostapd": {
+ "methods": [ "apsta_state" ]
}
},
"publish": [ "hostapd", "hostapd.*", "wpa_supplicant", "wpa_supplicant.*" ],