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