hostapd: allow adding initial AP without breaking STA interface connection
[openwrt/staging/hauke.git] / package / network / services / hostapd / files / hostapd.uc
1 let libubus = require("ubus");
2 import { open, readfile } from "fs";
3 import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } from "common";
4
5 let ubus = libubus.connect();
6
7 hostapd.data.config = {};
8
9 hostapd.data.file_fields = {
10 vlan_file: true,
11 wpa_psk_file: true,
12 accept_mac_file: true,
13 deny_mac_file: true,
14 eap_user_file: true,
15 ca_cert: true,
16 server_cert: true,
17 server_cert2: true,
18 private_key: true,
19 private_key2: true,
20 dh_file: true,
21 eap_sim_db: true,
22 };
23
24 function iface_remove(cfg)
25 {
26 if (!cfg || !cfg.bss || !cfg.bss[0] || !cfg.bss[0].ifname)
27 return;
28
29 hostapd.remove_iface(cfg.bss[0].ifname);
30 for (let bss in cfg.bss)
31 wdev_remove(bss.ifname);
32 }
33
34 function iface_gen_config(phy, config, start_disabled)
35 {
36 let str = `data:
37 ${join("\n", config.radio.data)}
38 channel=${config.radio.channel}
39 `;
40
41 for (let i = 0; i < length(config.bss); i++) {
42 let bss = config.bss[i];
43 let type = i > 0 ? "bss" : "interface";
44
45 str += `
46 ${type}=${bss.ifname}
47 ${join("\n", bss.data)}
48 `;
49 if (start_disabled)
50 str += `
51 start_disabled=1
52 `;
53 }
54
55 return str;
56 }
57
58 function iface_freq_info(iface, config, params)
59 {
60 let freq = params.frequency;
61 if (!freq)
62 return null;
63
64 let sec_offset = params.sec_chan_offset;
65 if (sec_offset != -1 && sec_offset != 1)
66 sec_offset = 0;
67
68 let width = 0;
69 for (let line in config.radio.data) {
70 if (!sec_offset && match(line, /^ht_capab=.*HT40/)) {
71 sec_offset = null; // auto-detect
72 continue;
73 }
74
75 let val = match(line, /^(vht_oper_chwidth|he_oper_chwidth)=(\d+)/);
76 if (!val)
77 continue;
78
79 val = int(val[2]);
80 if (val > width)
81 width = val;
82 }
83
84 if (freq < 4000)
85 width = 0;
86
87 return hostapd.freq_info(freq, sec_offset, width);
88 }
89
90 function iface_add(phy, config, phy_status)
91 {
92 let config_inline = iface_gen_config(phy, config, !!phy_status);
93
94 let bss = config.bss[0];
95 let ret = hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`);
96 if (ret < 0)
97 return false;
98
99 if (!phy_status)
100 return true;
101
102 let iface = hostapd.interfaces[bss.ifname];
103 if (!iface)
104 return false;
105
106 let freq_info = iface_freq_info(iface, config, phy_status);
107
108 return iface.start(freq_info) >= 0;
109 }
110
111 function iface_restart(phy, config, old_config)
112 {
113 iface_remove(old_config);
114 iface_remove(config);
115
116 if (!config.bss || !config.bss[0]) {
117 hostapd.printf(`No bss for phy ${phy}`);
118 return;
119 }
120
121 let bss = config.bss[0];
122 let err = wdev_create(phy, bss.ifname, { mode: "ap" });
123 if (err)
124 hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
125
126 let ubus = hostapd.data.ubus;
127 let phy_status = ubus.call("wpa_supplicant", "phy_status", { phy: phy });
128 if (phy_status && phy_status.state == "COMPLETED") {
129 if (iface_add(phy, config, phy_status))
130 return;
131
132 hostapd.printf(`Failed to bring up phy ${phy} ifname=${bss.ifname} with supplicant provided frequency`);
133 }
134
135 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
136 if (!iface_add(phy, config))
137 hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
138 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
139 }
140
141 function array_to_obj(arr, key, start)
142 {
143 let obj = {};
144
145 start ??= 0;
146 for (let i = start; i < length(arr); i++) {
147 let cur = arr[i];
148 obj[cur[key]] = cur;
149 }
150
151 return obj;
152 }
153
154 function find_array_idx(arr, key, val)
155 {
156 for (let i = 0; i < length(arr); i++)
157 if (arr[i][key] == val)
158 return i;
159
160 return -1;
161 }
162
163 function bss_reload_psk(bss, config, old_config)
164 {
165 if (is_equal(old_config.hash.wpa_psk_file, config.hash.wpa_psk_file))
166 return;
167
168 old_config.hash.wpa_psk_file = config.hash.wpa_psk_file;
169 if (!is_equal(old_config, config))
170 return;
171
172 let ret = bss.ctrl("RELOAD_WPA_PSK");
173 ret ??= "failed";
174
175 hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
176 }
177
178 function iface_reload_config(phy, config, old_config)
179 {
180 if (!old_config || !is_equal(old_config.radio, config.radio))
181 return false;
182
183 if (is_equal(old_config.bss, config.bss))
184 return true;
185
186 if (!old_config.bss || !old_config.bss[0])
187 return false;
188
189 if (config.bss[0].ifname != old_config.bss[0].ifname)
190 return false;
191
192 let iface_name = config.bss[0].ifname;
193 let iface = hostapd.interfaces[iface_name];
194 if (!iface)
195 return false;
196
197 let first_bss = hostapd.bss[iface_name];
198 if (!first_bss)
199 return false;
200
201 let config_inline = iface_gen_config(phy, config);
202
203 bss_reload_psk(first_bss, config.bss[0], old_config.bss[0]);
204 if (!is_equal(config.bss[0], old_config.bss[0])) {
205 if (phy_is_fullmac(phy))
206 return false;
207
208 if (config.bss[0].bssid != old_config.bss[0].bssid)
209 return false;
210
211 hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
212 if (first_bss.set_config(config_inline, 0) < 0) {
213 hostapd.printf(`Failed to set config`);
214 return false;
215 }
216 }
217
218 let new_cfg = array_to_obj(config.bss, "ifname", 1);
219 let old_cfg = array_to_obj(old_config.bss, "ifname", 1);
220
221 for (let name in old_cfg) {
222 let bss = hostapd.bss[name];
223 if (!bss) {
224 hostapd.printf(`bss '${name}' not found`);
225 return false;
226 }
227
228 if (!new_cfg[name]) {
229 hostapd.printf(`Remove bss '${name}' on phy '${phy}'`);
230 bss.delete();
231 wdev_remove(name);
232 continue;
233 }
234
235 let new_cfg_data = new_cfg[name];
236 delete new_cfg[name];
237
238 if (is_equal(old_cfg[name], new_cfg_data))
239 continue;
240
241 hostapd.printf(`Reload config for bss '${name}' on phy '${phy}'`);
242 let idx = find_array_idx(config.bss, "ifname", name);
243 if (idx < 0) {
244 hostapd.printf(`bss index not found`);
245 return false;
246 }
247
248 if (bss.set_config(config_inline, idx) < 0) {
249 hostapd.printf(`Failed to set config`);
250 return false;
251 }
252 }
253
254 for (let name in new_cfg) {
255 hostapd.printf(`Add bss '${name}' on phy '${phy}'`);
256
257 let idx = find_array_idx(config.bss, "ifname", name);
258 if (idx < 0) {
259 hostapd.printf(`bss index not found`);
260 return false;
261 }
262
263 if (iface.add_bss(config_inline, idx) < 0) {
264 hostapd.printf(`Failed to add bss`);
265 return false;
266 }
267 }
268
269 return true;
270 }
271
272 function iface_set_config(phy, config)
273 {
274 let old_config = hostapd.data.config[phy];
275
276 hostapd.data.config[phy] = config;
277
278 if (!config)
279 return iface_remove(old_config);
280
281 let ret = iface_reload_config(phy, config, old_config);
282 if (ret) {
283 hostapd.printf(`Reloaded settings for phy ${phy}`);
284 return 0;
285 }
286
287 hostapd.printf(`Restart interface for phy ${phy}`);
288 return iface_restart(phy, config, old_config);
289 }
290
291 function config_add_bss(config, name)
292 {
293 let bss = {
294 ifname: name,
295 data: [],
296 hash: {}
297 };
298
299 push(config.bss, bss);
300
301 return bss;
302 }
303
304 function iface_load_config(filename)
305 {
306 let f = open(filename, "r");
307 if (!f)
308 return null;
309
310 let config = {
311 radio: {
312 data: []
313 },
314 bss: [],
315 orig_file: filename,
316 };
317
318 let bss;
319 let line;
320 while ((line = trim(f.read("line"))) != null) {
321 let val = split(line, "=", 2);
322 if (!val[0])
323 continue;
324
325 if (val[0] == "interface") {
326 bss = config_add_bss(config, val[1]);
327 break;
328 }
329
330 if (val[0] == "channel") {
331 config.radio.channel = val[1];
332 continue;
333 }
334
335 push(config.radio.data, line);
336 }
337
338 while ((line = trim(f.read("line"))) != null) {
339 let val = split(line, "=", 2);
340 if (!val[0])
341 continue;
342
343 if (val[0] == "bssid")
344 bss.bssid = val[1];
345
346 if (val[0] == "bss") {
347 bss = config_add_bss(config, val[1]);
348 continue;
349 }
350
351 if (hostapd.data.file_fields[val[0]])
352 bss.hash[val[0]] = hostapd.sha1(readfile(val[1]));
353
354 push(bss.data, line);
355 }
356 f.close();
357
358 return config;
359 }
360
361
362 let main_obj = {
363 reload: {
364 args: {
365 phy: "",
366 },
367 call: function(req) {
368 try {
369 let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
370 for (let phy_name in phy_list) {
371 let phy = hostapd.data.config[phy_name];
372 let config = iface_load_config(phy.orig_file);
373 iface_set_config(phy_name, config);
374 }
375 } catch(e) {
376 hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
377 return libubus.STATUS_INVALID_ARGUMENT;
378 }
379
380 return 0;
381 }
382 },
383 apsta_state: {
384 args: {
385 phy: "",
386 up: true,
387 frequency: 0,
388 sec_chan_offset: 0,
389 csa: true,
390 csa_count: 0,
391 },
392 call: function(req) {
393 if (req.args.up == null || !req.args.phy)
394 return libubus.STATUS_INVALID_ARGUMENT;
395
396 let phy = req.args.phy;
397 let config = hostapd.data.config[phy];
398 if (!config || !config.bss || !config.bss[0] || !config.bss[0].ifname)
399 return 0;
400
401 let iface = hostapd.interfaces[config.bss[0].ifname];
402 if (!iface)
403 return 0;
404
405 if (!req.args.up) {
406 iface.stop();
407 return 0;
408 }
409
410 if (!req.args.frequency)
411 return libubus.STATUS_INVALID_ARGUMENT;
412
413 let freq_info = iface_freq_info(iface, config, req.args);
414 if (!freq_info)
415 return libubus.STATUS_UNKNOWN_ERROR;
416
417 let ret;
418 if (req.args.csa) {
419 freq_info.csa_count = req.args.csa_count ?? 10;
420 ret = iface.switch_channel(freq_info);
421 } else {
422 iface.stop();
423 ret = iface.start(freq_info);
424 }
425 if (!ret)
426 return libubus.STATUS_UNKNOWN_ERROR;
427
428 return 0;
429 }
430 },
431 config_set: {
432 args: {
433 phy: "",
434 config: "",
435 prev_config: "",
436 },
437 call: function(req) {
438 let phy = req.args.phy;
439 let file = req.args.config;
440 let prev_file = req.args.prev_config;
441
442 if (!phy)
443 return libubus.STATUS_INVALID_ARGUMENT;
444
445 try {
446 if (prev_file && !hostapd.data.config[phy]) {
447 let config = iface_load_config(prev_file);
448 if (config)
449 config.radio.data = [];
450 hostapd.data.config[phy] = config;
451 }
452
453 let config = iface_load_config(file);
454
455 hostapd.printf(`Set new config for phy ${phy}: ${file}`);
456 iface_set_config(phy, config);
457 } catch(e) {
458 hostapd.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
459 return libubus.STATUS_INVALID_ARGUMENT;
460 }
461
462 return {
463 pid: hostapd.getpid()
464 };
465 }
466 },
467 config_add: {
468 args: {
469 iface: "",
470 config: "",
471 },
472 call: function(req) {
473 if (!req.args.iface || !req.args.config)
474 return libubus.STATUS_INVALID_ARGUMENT;
475
476 if (hostapd.add_iface(`bss_config=${req.args.iface}:${req.args.config}`) < 0)
477 return libubus.STATUS_INVALID_ARGUMENT;
478
479 return {
480 pid: hostapd.getpid()
481 };
482 }
483 },
484 config_remove: {
485 args: {
486 iface: ""
487 },
488 call: function(req) {
489 if (!req.args.iface)
490 return libubus.STATUS_INVALID_ARGUMENT;
491
492 hostapd.remove_iface(req.args.iface);
493 return 0;
494 }
495 },
496 };
497
498 hostapd.data.ubus = ubus;
499 hostapd.data.obj = ubus.publish("hostapd", main_obj);
500
501 function bss_event(type, name, data) {
502 let ubus = hostapd.data.ubus;
503
504 data ??= {};
505 data.name = name;
506 hostapd.data.obj.notify(`bss.${type}`, data, null, null, null, -1);
507 ubus.call("service", "event", { type: `hostapd.${name}.${type}`, data: {} });
508 }
509
510 return {
511 shutdown: function() {
512 for (let phy in hostapd.data.config)
513 iface_set_config(phy, null);
514 hostapd.ubus.disconnect();
515 },
516 bss_add: function(name, obj) {
517 bss_event("add", name);
518 },
519 bss_reload: function(name, obj, reconf) {
520 bss_event("reload", name, { reconf: reconf != 0 });
521 },
522 bss_remove: function(name, obj) {
523 bss_event("remove", name);
524 }
525 };