mirror of
https://github.com/openwrt/openwrt.git
synced 2024-12-27 01:11:14 +00:00
e56c5f7b27
This implements vastly improved dynamic configuration reload support. It can handle configuration changes on individual wifi interfaces, as well as adding/removing interfaces. Signed-off-by: Felix Fietkau <nbd@nbd.name>
400 lines
8.1 KiB
Ucode
400 lines
8.1 KiB
Ucode
let libubus = require("ubus");
|
|
import { open, readfile } from "fs";
|
|
import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } 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)
|
|
{
|
|
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";
|
|
|
|
str += `
|
|
${type}=${bss.ifname}
|
|
${join("\n", bss.data)}
|
|
`;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
function iface_restart(phy, config, old_config)
|
|
{
|
|
iface_remove(old_config);
|
|
iface_remove(config);
|
|
|
|
if (!config.bss || !config.bss[0]) {
|
|
hostapd.printf(`No bss for phy ${phy}`);
|
|
return;
|
|
}
|
|
|
|
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 config_inline = iface_gen_config(phy, config);
|
|
if (hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`) < 0) {
|
|
hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 iface_reload_config(phy, config, old_config)
|
|
{
|
|
if (!old_config || !is_equal(old_config.radio, config.radio))
|
|
return false;
|
|
|
|
if (is_equal(old_config.bss, config.bss))
|
|
return true;
|
|
|
|
if (config.bss[0].ifname != old_config.bss[0].ifname)
|
|
return false;
|
|
|
|
let iface = hostapd.interfaces[config.bss[0].ifname];
|
|
if (!iface)
|
|
return false;
|
|
|
|
let config_inline = iface_gen_config(phy, config);
|
|
|
|
bss_reload_psk(iface.bss[0], config.bss[0], old_config.bss[0]);
|
|
if (!is_equal(config.bss[0], old_config.bss[0])) {
|
|
if (phy_is_fullmac(phy))
|
|
return false;
|
|
|
|
hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
|
|
if (iface.bss[0].set_config(config_inline, 0) < 0) {
|
|
hostapd.printf(`Failed to set config`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let bss_list = array_to_obj(iface.bss, "name", 1);
|
|
let new_cfg = array_to_obj(config.bss, "ifname", 1);
|
|
let old_cfg = array_to_obj(old_config.bss, "ifname", 1);
|
|
|
|
for (let name in old_cfg) {
|
|
let bss = bss_list[name];
|
|
if (!bss) {
|
|
hostapd.printf(`bss '${name}' not found`);
|
|
return false;
|
|
}
|
|
|
|
if (!new_cfg[name]) {
|
|
hostapd.printf(`Remove bss '${name}' on phy '${phy}'`);
|
|
bss.delete();
|
|
wdev_remove(name);
|
|
continue;
|
|
}
|
|
|
|
let new_cfg_data = new_cfg[name];
|
|
delete new_cfg[name];
|
|
|
|
if (is_equal(old_cfg[name], new_cfg_data))
|
|
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;
|
|
}
|
|
|
|
if (bss.set_config(config_inline, idx) < 0) {
|
|
hostapd.printf(`Failed to set config`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (let name in new_cfg) {
|
|
hostapd.printf(`Add 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;
|
|
}
|
|
|
|
if (iface.add_bss(config_inline, idx) < 0) {
|
|
hostapd.printf(`Failed to add bss`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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 ret = iface_reload_config(phy, config, old_config);
|
|
if (ret) {
|
|
hostapd.printf(`Reloaded settings for phy ${phy}`);
|
|
return 0;
|
|
}
|
|
|
|
hostapd.printf(`Restart interface for phy ${phy}`);
|
|
return iface_restart(phy, config, old_config);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
push(config.radio.data, line);
|
|
}
|
|
|
|
while ((line = trim(f.read("line"))) != null) {
|
|
let val = split(line, "=", 2);
|
|
if (!val[0])
|
|
continue;
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
},
|
|
config_set: {
|
|
args: {
|
|
phy: "",
|
|
config: "",
|
|
prev_config: "",
|
|
},
|
|
call: 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
return {
|
|
pid: hostapd.getpid()
|
|
};
|
|
}
|
|
},
|
|
config_add: {
|
|
args: {
|
|
iface: "",
|
|
config: "",
|
|
},
|
|
call: 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: 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);
|
|
}
|
|
};
|