import * as nl80211 from "nl80211";
import * as rtnl from "rtnl";
-import { readfile } from "fs";
+import { readfile, glob, basename, readlink } from "fs";
const iftypes = {
ap: nl80211.const.NL80211_IFTYPE_AP,
return join(":", map(addr, (val) => sprintf("%02x", val)));
}
-function wdev_generate_macaddr(phy, data)
+function wdev_macaddr(wdev)
{
- 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;
+ return trim(readfile(`/sys/class/net/${wdev}/address`));
+}
- let base_addr = phy_sysfs_file(phy, "macaddress");
- if (!base_addr)
- return null;
+const phy_proto = {
+ macaddr_init: function(used, options) {
+ this.macaddr_options = options ?? {};
+ this.macaddr_list = {};
- if (!idx && !mbssid)
- return base_addr;
+ 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;
- let base_mask = phy_sysfs_file(phy, "address_mask");
- if (!base_mask)
- return null;
+ this.for_each_wdev((wdev) => {
+ let macaddr = wdev_macaddr(wdev);
+ this.macaddr_list[macaddr] ??= -1;
+ });
- if (base_mask == "00:00:00:00:00:00" && idx >= num_global) {
- let addrs = split(phy_sysfs_file(phy, "addresses"), "\n");
+ return this.macaddr_list;
+ },
- if (idx < length(addrs))
- return addrs[idx];
+ 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;
- base_mask = "ff:ff:ff:ff:ff:ff";
- }
+ 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;
- 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)
+ 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[5] ^= idx;
- break;
- default:
- for (let i = 5; i > 0; i--) {
- addr[i] += idx;
- if (addr[i] < 256)
- break;
- addr[i] %= 256;
+ 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);
}
- break;
}
+};
- return macaddr_join(addr);
+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 = {
}, vlist_proto);
}
-export { wdev_remove, wdev_create, wdev_generate_macaddr, is_equal, vlist_new, phy_is_fullmac };
+export { wdev_remove, wdev_create, is_equal, vlist_new, phy_is_fullmac, phy_open };
let libubus = require("ubus");
import { open, readfile } from "fs";
-import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } from "common";
+import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open } from "common";
let ubus = libubus.connect();
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 += `
return iface.start(freq_info) >= 0;
}
-function iface_restart(phy, config, old_config)
+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);
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(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
}
-function iface_reload_config(phy, config, old_config)
+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;
+
+ 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 (!old_config.bss || !old_config.bss[0])
return false;
- if (config.bss[0].ifname != old_config.bss[0].ifname)
- return false;
-
- let iface_name = config.bss[0].ifname;
+ let iface_name = old_config.bss[0].ifname;
let iface = hostapd.interfaces[iface_name];
- if (!iface)
+ if (!iface) {
+ hostapd.printf(`Could not find previous interface ${iface_name}`);
return false;
+ }
let first_bss = hostapd.bss[iface_name];
- if (!first_bss)
+ if (!first_bss) {
+ hostapd.printf(`Could not find bss of previous interface ${iface_name}`);
return false;
+ }
- let config_inline = iface_gen_config(phy, config);
+ let macaddr_list = iface_config_macaddr_list(config);
+ let bss_list = [];
+ let bss_list_cfg = [];
+ let prev_bss_hash = [];
- bss_reload_psk(first_bss, config.bss[0], old_config.bss[0]);
- if (!is_equal(config.bss[0], old_config.bss[0])) {
- if (phy_is_fullmac(phy))
+ 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;
- if (config.bss[0].bssid != old_config.bss[0].bssid)
+ // 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;
- hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
- if (first_bss.set_config(config_inline, 0) < 0) {
- hostapd.printf(`Failed to set config`);
+ 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);
}
- let new_cfg = array_to_obj(config.bss, "ifname", 1);
- let old_cfg = array_to_obj(old_config.bss, "ifname", 1);
+ // 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;
- for (let name in old_cfg) {
- let bss = hostapd.bss[name];
- if (!bss) {
- hostapd.printf(`bss '${name}' not found`);
+ 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;
}
- if (!new_cfg[name]) {
- hostapd.printf(`Remove bss '${name}' on phy '${phy}'`);
- bss.delete();
- wdev_remove(name);
- continue;
+ 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;
+ }
- let new_cfg_data = new_cfg[name];
- delete new_cfg[name];
+ // 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];
- if (is_equal(old_cfg[name], new_cfg_data))
+ let mac_idx = macaddr_list[bsscfg.bssid];
+ if (mac_idx < 0)
+ macaddr_list[bsscfg.bssid] = i;
+ if (mac_idx == i)
continue;
- hostapd.printf(`Reload config for bss '${name}' on phy '${phy}'`);
- let idx = find_array_idx(config.bss, "ifname", name);
- if (idx < 0) {
- hostapd.printf(`bss index not found`);
- return false;
+ // 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];
}
- if (bss.set_config(config_inline, idx) < 0) {
- hostapd.printf(`Failed to set config`);
+ let addr = phydev.macaddr_next(i);
+ if (!addr) {
+ hostapd.printf(`Failed to generate mac address for phy ${phy}`);
return false;
}
+ bsscfg.bssid = addr;
}
- for (let name in new_cfg) {
- hostapd.printf(`Add bss '${name}' on phy '${phy}'`);
+ 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;
- let idx = find_array_idx(config.bss, "ifname", name);
- if (idx < 0) {
- hostapd.printf(`bss index not found`);
+ 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(`Failed to update config data files for bss ${ifname}`);
+ return false;
+ }
+ bss.ctrl("RELOAD_WPA_PSK");
+ continue;
+ }
- if (iface.add_bss(config_inline, idx) < 0) {
- hostapd.printf(`Failed to add bss`);
+ 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}'`);
+ hostapd.printf(`old: ${bss_remove_file_fields(bss_list_cfg[i])}`);
+ hostapd.printf(`new: ${bss_remove_file_fields(config.bss[i])}`);
+ 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];
if (!config)
return iface_remove(old_config);
- let ret = iface_reload_config(phy, config, old_config);
- if (ret) {
- hostapd.printf(`Reloaded settings for phy ${phy}`);
- return 0;
+ 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}`);
- return iface_restart(phy, config, old_config);
+ let ret = iface_restart(phydev, config, old_config);
+ iface_update_supplicant_macaddr(phy, config);
+
+ return ret;
}
function config_add_bss(config, name)
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 = val[1];
+ 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]);
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: function(req) {
- try {
- 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);
- }
- } catch(e) {
- hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
- return libubus.STATUS_INVALID_ARGUMENT;
+ 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: {
csa: true,
csa_count: 0,
},
- call: function(req) {
+ call: ex_wrap(function(req) {
if (req.args.up == null || !req.args.phy)
return libubus.STATUS_INVALID_ARGUMENT;
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: {
config: "",
prev_config: "",
},
- call: function(req) {
+ 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;
- try {
- if (prev_file && !hostapd.data.config[phy]) {
- let config = iface_load_config(prev_file);
- if (config)
- config.radio.data = [];
- hostapd.data.config[phy] = config;
- }
+ 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);
+ let config = iface_load_config(file);
- hostapd.printf(`Set new config for phy ${phy}: ${file}`);
- iface_set_config(phy, config);
- } catch(e) {
- hostapd.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
- return libubus.STATUS_INVALID_ARGUMENT;
- }
+ hostapd.printf(`Set new config for phy ${phy}: ${file}`);
+ iface_set_config(phy, config);
return {
pid: hostapd.getpid()
};
- }
+ })
},
config_add: {
args: {
iface: "",
config: "",
},
- call: function(req) {
+ call: ex_wrap(function(req) {
if (!req.args.iface || !req.args.config)
return libubus.STATUS_INVALID_ARGUMENT;
return {
pid: hostapd.getpid()
};
- }
+ })
},
config_remove: {
args: {
iface: ""
},
- call: function(req) {
+ call: ex_wrap(function(req) {
if (!req.args.iface)
return libubus.STATUS_INVALID_ARGUMENT;
hostapd.remove_iface(req.args.iface);
return 0;
- }
+ })
},
};