hostapd: rework reload support and MAC address handling
[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, phy_open } 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 let nasid = bss.nasid ?? replace(bss.bssid, ":", "");
45
46 str += `
47 ${type}=${bss.ifname}
48 bssid=${bss.bssid}
49 ${join("\n", bss.data)}
50 nas_identifier=${nasid}
51 `;
52 if (start_disabled)
53 str += `
54 start_disabled=1
55 `;
56 }
57
58 return str;
59 }
60
61 function iface_freq_info(iface, config, params)
62 {
63 let freq = params.frequency;
64 if (!freq)
65 return null;
66
67 let sec_offset = params.sec_chan_offset;
68 if (sec_offset != -1 && sec_offset != 1)
69 sec_offset = 0;
70
71 let width = 0;
72 for (let line in config.radio.data) {
73 if (!sec_offset && match(line, /^ht_capab=.*HT40/)) {
74 sec_offset = null; // auto-detect
75 continue;
76 }
77
78 let val = match(line, /^(vht_oper_chwidth|he_oper_chwidth)=(\d+)/);
79 if (!val)
80 continue;
81
82 val = int(val[2]);
83 if (val > width)
84 width = val;
85 }
86
87 if (freq < 4000)
88 width = 0;
89
90 return hostapd.freq_info(freq, sec_offset, width);
91 }
92
93 function iface_add(phy, config, phy_status)
94 {
95 let config_inline = iface_gen_config(phy, config, !!phy_status);
96
97 let bss = config.bss[0];
98 let ret = hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`);
99 if (ret < 0)
100 return false;
101
102 if (!phy_status)
103 return true;
104
105 let iface = hostapd.interfaces[bss.ifname];
106 if (!iface)
107 return false;
108
109 let freq_info = iface_freq_info(iface, config, phy_status);
110
111 return iface.start(freq_info) >= 0;
112 }
113
114 function iface_config_macaddr_list(config)
115 {
116 let macaddr_list = {};
117 for (let i = 0; i < length(config.bss); i++) {
118 let bss = config.bss[i];
119 if (!bss.default_macaddr)
120 macaddr_list[bss.bssid] = i;
121 }
122
123 return macaddr_list;
124 }
125
126 function iface_restart(phydev, config, old_config)
127 {
128 let phy = phydev.name;
129
130 iface_remove(old_config);
131 iface_remove(config);
132
133 if (!config.bss || !config.bss[0]) {
134 hostapd.printf(`No bss for phy ${phy}`);
135 return;
136 }
137
138 phydev.macaddr_init(iface_config_macaddr_list(config));
139 for (let i = 0; i < length(config.bss); i++) {
140 let bss = config.bss[i];
141 if (bss.default_macaddr)
142 bss.bssid = phydev.macaddr_next();
143 }
144
145 let bss = config.bss[0];
146 let err = wdev_create(phy, bss.ifname, { mode: "ap" });
147 if (err)
148 hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
149
150 let ubus = hostapd.data.ubus;
151 let phy_status = ubus.call("wpa_supplicant", "phy_status", { phy: phy });
152 if (phy_status && phy_status.state == "COMPLETED") {
153 if (iface_add(phy, config, phy_status))
154 return;
155
156 hostapd.printf(`Failed to bring up phy ${phy} ifname=${bss.ifname} with supplicant provided frequency`);
157 }
158
159 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
160 if (!iface_add(phy, config))
161 hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
162 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
163 }
164
165 function array_to_obj(arr, key, start)
166 {
167 let obj = {};
168
169 start ??= 0;
170 for (let i = start; i < length(arr); i++) {
171 let cur = arr[i];
172 obj[cur[key]] = cur;
173 }
174
175 return obj;
176 }
177
178 function find_array_idx(arr, key, val)
179 {
180 for (let i = 0; i < length(arr); i++)
181 if (arr[i][key] == val)
182 return i;
183
184 return -1;
185 }
186
187 function bss_reload_psk(bss, config, old_config)
188 {
189 if (is_equal(old_config.hash.wpa_psk_file, config.hash.wpa_psk_file))
190 return;
191
192 old_config.hash.wpa_psk_file = config.hash.wpa_psk_file;
193 if (!is_equal(old_config, config))
194 return;
195
196 let ret = bss.ctrl("RELOAD_WPA_PSK");
197 ret ??= "failed";
198
199 hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
200 }
201
202 function remove_file_fields(config)
203 {
204 return filter(config, (line) => !hostapd.data.file_fields[split(line, "=")[0]]);
205 }
206
207 function bss_remove_file_fields(config)
208 {
209 let new_cfg = {};
210
211 for (let key in config)
212 new_cfg[key] = config[key];
213 new_cfg.data = remove_file_fields(new_cfg.data);
214 new_cfg.hash = {};
215 for (let key in config.hash)
216 new_cfg.hash[key] = config.hash[key];
217 delete new_cfg.hash.wpa_psk_file;
218
219 return new_cfg;
220 }
221
222 function bss_config_hash(config)
223 {
224 return hostapd.sha1(remove_file_fields(config) + "");
225 }
226
227 function bss_find_existing(config, prev_config, prev_hash)
228 {
229 let hash = bss_config_hash(config.data);
230
231 for (let i = 0; i < length(prev_config.bss); i++) {
232 if (!prev_hash[i] || hash != prev_hash[i])
233 continue;
234
235 prev_hash[i] = null;
236 return i;
237 }
238
239 return -1;
240 }
241
242 function get_config_bss(config, idx)
243 {
244 if (!config.bss[idx]) {
245 hostapd.printf(`Invalid bss index ${idx}`);
246 return null;
247 }
248
249 let ifname = config.bss[idx].ifname;
250 if (!ifname)
251 hostapd.printf(`Could not find bss ${config.bss[idx].ifname}`);
252
253 return hostapd.bss[ifname];
254 }
255
256 function iface_reload_config(phydev, config, old_config)
257 {
258 let phy = phydev.name;
259
260 if (!old_config || !is_equal(old_config.radio, config.radio))
261 return false;
262
263 if (is_equal(old_config.bss, config.bss))
264 return true;
265
266 if (!old_config.bss || !old_config.bss[0])
267 return false;
268
269 let iface_name = old_config.bss[0].ifname;
270 let iface = hostapd.interfaces[iface_name];
271 if (!iface) {
272 hostapd.printf(`Could not find previous interface ${iface_name}`);
273 return false;
274 }
275
276 let first_bss = hostapd.bss[iface_name];
277 if (!first_bss) {
278 hostapd.printf(`Could not find bss of previous interface ${iface_name}`);
279 return false;
280 }
281
282 let macaddr_list = iface_config_macaddr_list(config);
283 let bss_list = [];
284 let bss_list_cfg = [];
285 let prev_bss_hash = [];
286
287 for (let bss in old_config.bss) {
288 let hash = bss_config_hash(bss.data);
289 push(prev_bss_hash, bss_config_hash(bss.data));
290 }
291
292 // Step 1: find (possibly renamed) interfaces with the same config
293 // and store them in the new order (with gaps)
294 for (let i = 0; i < length(config.bss); i++) {
295 let prev;
296
297 // For fullmac devices, the first interface needs to be preserved,
298 // since it's treated as the master
299 if (!i && phy_is_fullmac(phy)) {
300 prev = 0;
301 prev_bss_hash[0] = null;
302 } else {
303 prev = bss_find_existing(config.bss[i], old_config, prev_bss_hash);
304 }
305 if (prev < 0)
306 continue;
307
308 let cur_config = config.bss[i];
309 let prev_config = old_config.bss[prev];
310
311 let prev_bss = get_config_bss(old_config, prev);
312 if (!prev_bss)
313 return false;
314
315 // try to preserve MAC address of this BSS by reassigning another
316 // BSS if necessary
317 if (cur_config.default_macaddr &&
318 !macaddr_list[prev_config.bssid]) {
319 macaddr_list[prev_config.bssid] = i;
320 cur_config.bssid = prev_config.bssid;
321 }
322
323 bss_list[i] = prev_bss;
324 bss_list_cfg[i] = old_config.bss[prev];
325 }
326
327 if (config.mbssid && !bss_list_cfg[0]) {
328 hostapd.printf("First BSS changed with MBSSID enabled");
329 return false;
330 }
331
332 // Step 2: if none were found, rename and preserve the first one
333 if (length(bss_list) == 0) {
334 // can't change the bssid of the first bss
335 if (config.bss[0].bssid != old_config.bss[0].bssid) {
336 if (!config.bss[0].default_macaddr) {
337 hostapd.printf(`BSSID of first interface changed: ${lc(old_config.bss[0].bssid)} -> ${lc(config.bss[0].bssid)}`);
338 return false;
339 }
340
341 config.bss[0].bssid = old_config.bss[0].bssid;
342 }
343
344 let prev_bss = get_config_bss(old_config, 0);
345 if (!prev_bss)
346 return false;
347
348 macaddr_list[config.bss[0].bssid] = 0;
349 bss_list[0] = prev_bss;
350 bss_list_cfg[0] = old_config.bss[0];
351 prev_bss_hash[0] = null;
352 }
353
354 // Step 3: delete all unused old interfaces
355 for (let i = 0; i < length(prev_bss_hash); i++) {
356 if (!prev_bss_hash[i])
357 continue;
358
359 let prev_bss = get_config_bss(old_config, i);
360 if (!prev_bss)
361 return false;
362
363 let ifname = old_config.bss[i].ifname;
364 hostapd.printf(`Remove bss '${ifname}' on phy '${phy}'`);
365 prev_bss.delete();
366 wdev_remove(ifname);
367 }
368
369 // Step 4: rename preserved interfaces, use temporary name on duplicates
370 let rename_list = [];
371 for (let i = 0; i < length(bss_list); i++) {
372 if (!bss_list[i])
373 continue;
374
375 let old_ifname = bss_list_cfg[i].ifname;
376 let new_ifname = config.bss[i].ifname;
377 if (old_ifname == new_ifname)
378 continue;
379
380 if (hostapd.bss[new_ifname]) {
381 new_ifname = "tmp_" + substr(hostapd.sha1(new_ifname), 0, 8);
382 push(rename_list, i);
383 }
384
385 hostapd.printf(`Rename bss ${old_ifname} to ${new_ifname}`);
386 if (!bss_list[i].rename(new_ifname)) {
387 hostapd.printf(`Failed to rename bss ${old_ifname} to ${new_ifname}`);
388 return false;
389 }
390
391 bss_list_cfg[i].ifname = new_ifname;
392 }
393
394 // Step 5: rename interfaces with temporary names
395 for (let i in rename_list) {
396 let new_ifname = config.bss[i].ifname;
397 if (!bss_list[i].rename(new_ifname)) {
398 hostapd.printf(`Failed to rename bss to ${new_ifname}`);
399 return false;
400 }
401 bss_list_cfg[i].ifname = new_ifname;
402 }
403
404 // Step 6: assign BSSID for newly created interfaces
405 let macaddr_data = {
406 num_global: config.num_global_macaddr ?? 1,
407 mbssid: config.mbssid ?? 0,
408 };
409 macaddr_list = phydev.macaddr_init(macaddr_list, macaddr_data);
410 for (let i = 0; i < length(config.bss); i++) {
411 if (bss_list[i])
412 continue;
413 let bsscfg = config.bss[i];
414
415 let mac_idx = macaddr_list[bsscfg.bssid];
416 if (mac_idx < 0)
417 macaddr_list[bsscfg.bssid] = i;
418 if (mac_idx == i)
419 continue;
420
421 // statically assigned bssid of the new interface is in conflict
422 // with the bssid of a reused interface. reassign the reused interface
423 if (!bsscfg.default_macaddr) {
424 // can't update bssid of the first BSS, need to restart
425 if (!mac_idx < 0)
426 return false;
427
428 bsscfg = config.bss[mac_idx];
429 }
430
431 let addr = phydev.macaddr_next(i);
432 if (!addr) {
433 hostapd.printf(`Failed to generate mac address for phy ${phy}`);
434 return false;
435 }
436 bsscfg.bssid = addr;
437 }
438
439 let config_inline = iface_gen_config(phy, config);
440
441 // Step 7: fill in the gaps with new interfaces
442 for (let i = 0; i < length(config.bss); i++) {
443 let ifname = config.bss[i].ifname;
444 let bss = bss_list[i];
445
446 if (bss)
447 continue;
448
449 hostapd.printf(`Add bss ${ifname} on phy ${phy}`);
450 bss_list[i] = iface.add_bss(config_inline, i);
451 if (!bss_list[i]) {
452 hostapd.printf(`Failed to add new bss ${ifname} on phy ${phy}`);
453 return false;
454 }
455 }
456
457 // Step 8: update interface bss order
458 if (!iface.set_bss_order(bss_list)) {
459 hostapd.printf(`Failed to update BSS order on phy '${phy}'`);
460 return false;
461 }
462
463 // Step 9: update config
464 for (let i = 0; i < length(config.bss); i++) {
465 if (!bss_list_cfg[i])
466 continue;
467
468 let ifname = config.bss[i].ifname;
469 let bss = bss_list[i];
470
471 if (is_equal(config.bss[i], bss_list_cfg[i]))
472 continue;
473
474 if (is_equal(bss_remove_file_fields(config.bss[i]),
475 bss_remove_file_fields(bss_list_cfg[i]))) {
476 hostapd.printf(`Update config data files for bss ${ifname}`);
477 if (bss.set_config(config_inline, i, true) < 0) {
478 hostapd.printf(`Failed to update config data files for bss ${ifname}`);
479 return false;
480 }
481 bss.ctrl("RELOAD_WPA_PSK");
482 continue;
483 }
484
485 bss_reload_psk(bss, config.bss[i], bss_list_cfg[i]);
486 if (is_equal(config.bss[i], bss_list_cfg[i]))
487 continue;
488
489 hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
490 hostapd.printf(`old: ${bss_remove_file_fields(bss_list_cfg[i])}`);
491 hostapd.printf(`new: ${bss_remove_file_fields(config.bss[i])}`);
492 if (bss.set_config(config_inline, i) < 0) {
493 hostapd.printf(`Failed to set config for bss ${ifname}`);
494 return false;
495 }
496 }
497
498 return true;
499 }
500
501 function iface_update_supplicant_macaddr(phy, config)
502 {
503 let macaddr_list = [];
504 for (let i = 0; i < length(config.bss); i++)
505 push(macaddr_list, config.bss[i].bssid);
506 ubus.call("wpa_supplicant", "phy_set_macaddr_list", { phy: phy, macaddr: macaddr_list });
507 }
508
509 function iface_set_config(phy, config)
510 {
511 let old_config = hostapd.data.config[phy];
512
513 hostapd.data.config[phy] = config;
514
515 if (!config)
516 return iface_remove(old_config);
517
518 let phydev = phy_open(phy);
519 if (!phydev) {
520 hostapd.printf(`Failed to open phy ${phy}`);
521 return false;
522 }
523
524 try {
525 let ret = iface_reload_config(phydev, config, old_config);
526 if (ret) {
527 iface_update_supplicant_macaddr(phy, config);
528 hostapd.printf(`Reloaded settings for phy ${phy}`);
529 return 0;
530 }
531 } catch (e) {
532 hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
533 }
534
535 hostapd.printf(`Restart interface for phy ${phy}`);
536 let ret = iface_restart(phydev, config, old_config);
537 iface_update_supplicant_macaddr(phy, config);
538
539 return ret;
540 }
541
542 function config_add_bss(config, name)
543 {
544 let bss = {
545 ifname: name,
546 data: [],
547 hash: {}
548 };
549
550 push(config.bss, bss);
551
552 return bss;
553 }
554
555 function iface_load_config(filename)
556 {
557 let f = open(filename, "r");
558 if (!f)
559 return null;
560
561 let config = {
562 radio: {
563 data: []
564 },
565 bss: [],
566 orig_file: filename,
567 };
568
569 let bss;
570 let line;
571 while ((line = trim(f.read("line"))) != null) {
572 let val = split(line, "=", 2);
573 if (!val[0])
574 continue;
575
576 if (val[0] == "interface") {
577 bss = config_add_bss(config, val[1]);
578 break;
579 }
580
581 if (val[0] == "channel") {
582 config.radio.channel = val[1];
583 continue;
584 }
585
586 if (val[0] == "#num_global_macaddr" ||
587 val[0] == "mbssid")
588 config[val[0]] = int(val[1]);
589
590 push(config.radio.data, line);
591 }
592
593 while ((line = trim(f.read("line"))) != null) {
594 if (line == "#default_macaddr")
595 bss.default_macaddr = true;
596
597 let val = split(line, "=", 2);
598 if (!val[0])
599 continue;
600
601 if (val[0] == "bssid") {
602 bss.bssid = lc(val[1]);
603 continue;
604 }
605
606 if (val[0] == "nas_identifier")
607 bss.nasid = val[1];
608
609 if (val[0] == "bss") {
610 bss = config_add_bss(config, val[1]);
611 continue;
612 }
613
614 if (hostapd.data.file_fields[val[0]])
615 bss.hash[val[0]] = hostapd.sha1(readfile(val[1]));
616
617 push(bss.data, line);
618 }
619 f.close();
620
621 return config;
622 }
623
624 function ex_wrap(func) {
625 return (req) => {
626 try {
627 let ret = func(req);
628 return ret;
629 } catch(e) {
630 hostapd.printf(`Exception in ubus function: ${e}\n${e.stacktrace[0].context}`);
631 }
632 return libubus.STATUS_UNKNOWN_ERROR;
633 };
634 }
635
636 let main_obj = {
637 reload: {
638 args: {
639 phy: "",
640 },
641 call: ex_wrap(function(req) {
642 let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
643 for (let phy_name in phy_list) {
644 let phy = hostapd.data.config[phy_name];
645 let config = iface_load_config(phy.orig_file);
646 iface_set_config(phy_name, config);
647 }
648
649 return 0;
650 })
651 },
652 apsta_state: {
653 args: {
654 phy: "",
655 up: true,
656 frequency: 0,
657 sec_chan_offset: 0,
658 csa: true,
659 csa_count: 0,
660 },
661 call: ex_wrap(function(req) {
662 if (req.args.up == null || !req.args.phy)
663 return libubus.STATUS_INVALID_ARGUMENT;
664
665 let phy = req.args.phy;
666 let config = hostapd.data.config[phy];
667 if (!config || !config.bss || !config.bss[0] || !config.bss[0].ifname)
668 return 0;
669
670 let iface = hostapd.interfaces[config.bss[0].ifname];
671 if (!iface)
672 return 0;
673
674 if (!req.args.up) {
675 iface.stop();
676 return 0;
677 }
678
679 if (!req.args.frequency)
680 return libubus.STATUS_INVALID_ARGUMENT;
681
682 let freq_info = iface_freq_info(iface, config, req.args);
683 if (!freq_info)
684 return libubus.STATUS_UNKNOWN_ERROR;
685
686 let ret;
687 if (req.args.csa) {
688 freq_info.csa_count = req.args.csa_count ?? 10;
689 ret = iface.switch_channel(freq_info);
690 } else {
691 iface.stop();
692 ret = iface.start(freq_info);
693 }
694 if (!ret)
695 return libubus.STATUS_UNKNOWN_ERROR;
696
697 return 0;
698 })
699 },
700 config_get_macaddr_list: {
701 args: {
702 phy: ""
703 },
704 call: ex_wrap(function(req) {
705 let phy = req.args.phy;
706 if (!phy)
707 return libubus.STATUS_INVALID_ARGUMENT;
708
709 let ret = {
710 macaddr: [],
711 };
712
713 let config = hostapd.data.config[phy];
714 if (!config)
715 return ret;
716
717 ret.macaddr = map(config.bss, (bss) => bss.bssid);
718 return ret;
719 })
720 },
721 config_set: {
722 args: {
723 phy: "",
724 config: "",
725 prev_config: "",
726 },
727 call: ex_wrap(function(req) {
728 let phy = req.args.phy;
729 let file = req.args.config;
730 let prev_file = req.args.prev_config;
731
732 if (!phy)
733 return libubus.STATUS_INVALID_ARGUMENT;
734
735 if (prev_file && !hostapd.data.config[phy]) {
736 let config = iface_load_config(prev_file);
737 if (config)
738 config.radio.data = [];
739 hostapd.data.config[phy] = config;
740 }
741
742 let config = iface_load_config(file);
743
744 hostapd.printf(`Set new config for phy ${phy}: ${file}`);
745 iface_set_config(phy, config);
746
747 return {
748 pid: hostapd.getpid()
749 };
750 })
751 },
752 config_add: {
753 args: {
754 iface: "",
755 config: "",
756 },
757 call: ex_wrap(function(req) {
758 if (!req.args.iface || !req.args.config)
759 return libubus.STATUS_INVALID_ARGUMENT;
760
761 if (hostapd.add_iface(`bss_config=${req.args.iface}:${req.args.config}`) < 0)
762 return libubus.STATUS_INVALID_ARGUMENT;
763
764 return {
765 pid: hostapd.getpid()
766 };
767 })
768 },
769 config_remove: {
770 args: {
771 iface: ""
772 },
773 call: ex_wrap(function(req) {
774 if (!req.args.iface)
775 return libubus.STATUS_INVALID_ARGUMENT;
776
777 hostapd.remove_iface(req.args.iface);
778 return 0;
779 })
780 },
781 };
782
783 hostapd.data.ubus = ubus;
784 hostapd.data.obj = ubus.publish("hostapd", main_obj);
785
786 function bss_event(type, name, data) {
787 let ubus = hostapd.data.ubus;
788
789 data ??= {};
790 data.name = name;
791 hostapd.data.obj.notify(`bss.${type}`, data, null, null, null, -1);
792 ubus.call("service", "event", { type: `hostapd.${name}.${type}`, data: {} });
793 }
794
795 return {
796 shutdown: function() {
797 for (let phy in hostapd.data.config)
798 iface_set_config(phy, null);
799 hostapd.ubus.disconnect();
800 },
801 bss_add: function(name, obj) {
802 bss_event("add", name);
803 },
804 bss_reload: function(name, obj, reconf) {
805 bss_event("reload", name, { reconf: reconf != 0 });
806 },
807 bss_remove: function(name, obj) {
808 bss_event("remove", name);
809 }
810 };