]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
netifd: update to Git HEAD (2025-08-02)
authorFelix Fietkau <nbd@nbd.name>
Sat, 2 Aug 2025 14:45:39 +0000 (16:45 +0200)
committerFelix Fietkau <nbd@nbd.name>
Sat, 2 Aug 2025 14:46:31 +0000 (16:46 +0200)
3a7878065829 system-dummy: add missing vrf functions
471d9d6abb6d CMakeLists.txt: bump minimum required version
c3a0255e2150 scripts: fix dummy mode on systems where libubox is in /usr/local
7a3b281230e4 update example mac80211 script and wireless config
d9f2dd2614f2 wireless: replace with ucode scripts
74c22601baad wireless: add MLO support to example scripts

Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/network/config/netifd/Makefile
package/network/config/netifd/files/lib/netifd/main.uc [new file with mode: 0644]
package/network/config/netifd/files/lib/netifd/utils.uc [new file with mode: 0644]
package/network/config/wifi-scripts/files/lib/netifd/wireless-device.uc [new file with mode: 0644]
package/network/config/wifi-scripts/files/lib/netifd/wireless.uc [new file with mode: 0644]

index 86a2713e81395ba37619be1cfc4e3beaeb957169..790619b6a3a710cbdc1b28dca01c4f70aea5dd5f 100644 (file)
@@ -5,9 +5,9 @@ PKG_RELEASE:=1
 
 PKG_SOURCE_PROTO:=git
 PKG_SOURCE_URL=$(PROJECT_GIT)/project/netifd.git
-PKG_SOURCE_DATE:=2025-05-23
-PKG_SOURCE_VERSION:=7901e66c5f273bceee8981bc8a0c8b0e60945f60
-PKG_MIRROR_HASH:=8b85ec64e446ae065b1466c520b2d3aae329b6167221e425af903777278f557e
+PKG_SOURCE_DATE:=2025-08-02
+PKG_SOURCE_VERSION:=74c22601baad83cc9bc0fddb98f15d7abaa52c67
+PKG_MIRROR_HASH:=3841d17a0e59bab8dbcf0433bbc326f5b7b9b54dbb79b8759c20bdb5f0340227
 PKG_MAINTAINER:=Felix Fietkau <nbd@nbd.name>
 
 PKG_LICENSE:=GPL-2.0
diff --git a/package/network/config/netifd/files/lib/netifd/main.uc b/package/network/config/netifd/files/lib/netifd/main.uc
new file mode 100644 (file)
index 0000000..6c2e38f
--- /dev/null
@@ -0,0 +1,80 @@
+import * as uci from "uci";
+import * as uloop from "uloop";
+import * as libubus from "ubus";
+import { access, dirname } from "fs";
+
+function ex_handler(e)
+{
+       netifd.log(netifd.L_WARNING, `Exception: ${e}\n${e.stacktrace[0].context}\n`);
+}
+
+uloop.guard(ex_handler);
+libubus.guard(ex_handler);
+
+let ubus = netifd.ubus = libubus.connect();
+let wireless;
+
+function uci_ctx()
+{
+       let savedir = netifd.dummy_mode ? "./tmp" : null;
+       let ctx = uci.cursor(netifd.config_path, savedir, null, {
+               strict: false
+       });
+       return ctx;
+}
+
+function config_init()
+{
+       let ctx = uci_ctx();
+
+       if (wireless)
+               wireless.config_init(ctx);
+}
+
+function config_start()
+{
+       if (wireless)
+               wireless.config_start();
+}
+
+function check_interfaces()
+{
+       if (wireless)
+               wireless.check_interfaces();
+}
+
+function hotplug(name, add)
+{
+       if (wireless)
+               wireless.hotplug(name, add);
+}
+
+function ex_wrap(cb)
+{
+       let fn = cb;
+       return (...args) => {
+               try {
+                       return fn(...args);
+               } catch (e) {
+                       netifd.log(netifd.L_WARNING, `${e}\n${e.stacktrace[0].context}`);
+               }
+       };
+}
+
+netifd.cb = {
+       hotplug: ex_wrap(hotplug),
+       config_init: ex_wrap(config_init),
+       config_start: ex_wrap(config_start),
+       check_interfaces: ex_wrap(check_interfaces),
+};
+
+const wireless_module = dirname(sourcepath()) + "/wireless.uc";
+if (access(wireless_module, "r")) {
+       try {
+               wireless = loadfile(wireless_module)();
+       } catch (e) {
+               netifd.log(netifd.L_WARNING, `Error loading wireless module: ${e}\n${e.stacktrace[0].context}\n`);
+       }
+} else {
+       netifd.log(netifd.L_WARNING, `Wireless module not found\n`);
+}
diff --git a/package/network/config/netifd/files/lib/netifd/utils.uc b/package/network/config/netifd/files/lib/netifd/utils.uc
new file mode 100644 (file)
index 0000000..84db69d
--- /dev/null
@@ -0,0 +1,131 @@
+'use strict';
+
+import { glob, basename, realpath, chdir, mkstemp } from "fs";
+
+export const TYPE_ARRAY = 1;
+export const TYPE_STRING = 3;
+export const TYPE_INT = 5;
+export const TYPE_BOOL = 7;
+
+export function parse_bool(val)
+{
+       switch (val) {
+       case "1":
+       case "true":
+               return true;
+       case "0":
+       case "false":
+               return false;
+       }
+};
+
+export function parse_array(val)
+{
+       if (type(val) != "array")
+               val = split(val, /\s+/);
+       return val;
+};
+
+function __type_parsers()
+{
+       let ret = [];
+
+       ret[TYPE_ARRAY] = parse_array;
+       ret[TYPE_STRING] = function(val) {
+               return val;
+       };
+       ret[TYPE_INT] = function(val) {
+               return +val;
+       };
+       ret[TYPE_BOOL] = parse_bool;
+
+       return ret;
+}
+export const type_parser = __type_parsers();
+
+export function handler_load(path, cb)
+{
+       for (let script in glob(path + "/*.sh")) {
+               script = basename(script);
+
+               let f = mkstemp();
+               let prev_dir = realpath(".");
+               chdir(path);
+               system(`./${script} "" "dump" >&${f.fileno()}`);
+               chdir(prev_dir);
+               f.seek();
+               while (!f.error()) {
+                       let data = trim(f.read("line"));
+                       try {
+                               data = json(data);
+                       } catch (e) {
+                               continue;
+                       }
+
+                       if (type(data) != "object")
+                               continue;
+
+                       cb(script, data);
+               }
+               f.close();
+       }
+};
+
+export function handler_attributes(data, extra, validate)
+{
+       let ret = { ...extra };
+       for (let cur in data) {
+               let name_data = split(cur[0], ":", 2);
+               let name = name_data[0];
+               ret[name] = cur[1];
+               if (validate && name_data[1])
+                       validate[name] = name_data[1];
+       }
+       return ret;
+};
+
+export function parse_attribute_list(data, spec)
+{
+       let ret = {};
+
+       for (let name, type_id in spec) {
+               if (!(name in data))
+                       continue;
+
+               let val = data[name];
+               let parser = type_parser[type_id];
+               if (parser)
+                       val = parser(val);
+               ret[name] = val;
+       }
+
+       return ret;
+};
+
+export function is_equal(val1, val2) {
+       let t1 = type(val1);
+
+       if (t1 != type(val2))
+               return false;
+
+       if (t1 == "array") {
+               if (length(val1) != length(val2))
+                       return false;
+
+               for (let i = 0; i < length(val1); i++)
+                       if (!is_equal(val1[i], val2[i]))
+                               return false;
+
+               return true;
+       } else if (t1 == "object") {
+               for (let key in val1)
+                       if (!is_equal(val1[key], val2[key]))
+                               return false;
+               for (let key in val2)
+                       if (val1[key] == null)
+                               return false;
+               return true;
+       } else {
+               return val1 == val2;
+       }
+};
diff --git a/package/network/config/wifi-scripts/files/lib/netifd/wireless-device.uc b/package/network/config/wifi-scripts/files/lib/netifd/wireless-device.uc
new file mode 100644 (file)
index 0000000..b0faef1
--- /dev/null
@@ -0,0 +1,637 @@
+'use strict';
+import * as libubus from "ubus";
+import * as uloop from "uloop";
+import { is_equal } from "./utils.uc";
+import { access } from "fs";
+
+const NOTIFY_CMD_UP = 0;
+const NOTIFY_CMD_SET_DATA = 1;
+const NOTIFY_CMD_PROCESS_ADD = 2;
+const NOTIFY_CMD_SET_RETRY = 4;
+
+const DEFAULT_RETRY = 3;
+const DEFAULT_SCRIPT_TIMEOUT = 30 * 1000;
+
+export const mlo_name = "#mlo";
+
+let mlo_wdev;
+let wdev_cur;
+let wdev_handler = {};
+let wdev_script_task, wdev_script_timeout;
+let handler_timer;
+
+function delete_wdev(name)
+{
+       delete netifd.wireless.devices[name];
+       gc();
+}
+
+function handle_link(dev, data, up)
+{
+       let config = data.config;
+       let bridge_isolate;
+       let ap = false;
+       if (dev == data.ifname)
+               ap = data.type == "vlan" ||
+                    (data.type == "vif" && config.mode == "ap");
+
+       let dev_data = {
+               isolate: !!config.bridge_isolate,
+               wireless: true,
+               wireless_ap: ap,
+       };
+
+       if (ap && config.multicast_to_unicast != null)
+               dev_data.multicast_to_unicast = config.multicast_to_unicast;
+
+       if (data.type == "vif" && config.mode == "ap") {
+               dev_data.wireless_proxyarp = !!config.proxy_arp;
+               dev_data.wireless_isolate = !!config.isolate;
+       }
+
+       if (up)
+               netifd.device_set(dev, dev_data);
+
+       for (let net in config.network)
+               netifd.interface_handle_link({
+                       name: net,
+                       ifname: dev,
+                       vlan: config.network_vlan,
+                       link_ext: true,
+                       up,
+               });
+}
+
+function wdev_mlo_fixup(config)
+{
+       if (!mlo_wdev)
+               return;
+
+       for (let name, iface in config.interfaces) {
+               let config = iface.config;
+
+               if (config.mode != "link")
+                       continue;
+
+               let mlo_config = mlo_wdev.handler_data[iface.name];
+               if (mlo_config && mlo_config.ifname)
+                       config.ifname = mlo_config.ifname;
+       }
+}
+
+function wdev_config_init(wdev)
+{
+       let data = wdev.data;
+       let config = data.config;
+       let interfaces = {};
+
+       let vif_idx = 0;
+       for (let vif in data.vif) {
+               let vlan_idx = 0, sta_idx = 0;
+               let vlans = {}, stas = {};
+
+               if (wdev.disabled_vifs[vif.name])
+                       continue;
+               for (let vlan in vif.vlan) {
+                       let vlan_name = sprintf("%02d", ++vlan_idx);
+                       let cur_vlan = vlans[vlan_name] = {
+                               name: vlan.name,
+                               config: vlan.config,
+                       };
+
+                       if (wdev.disabled_vifs[vif.name])
+                               continue;
+                       for (let net in vlan.config.network)
+                               if (netifd.interface_get_bridge(net, cur_vlan))
+                                       break;
+               }
+
+               for (let sta in vif.sta) {
+                       let sta_name = sprintf("%02d", ++sta_idx);
+                       stas[sta_name] = {
+                               name: sta.name,
+                               config: sta.config,
+                       };
+               }
+
+               let vif_name = sprintf("%02d", ++vif_idx);
+               let iface = interfaces[vif_name] = {
+                       name: vif.name,
+                       config: vif.config,
+                       vlans, stas,
+               };
+
+               for (let net in vif.config.network)
+                       if (netifd.interface_get_bridge(net, iface))
+                               break;
+       }
+
+       wdev.handler_config = {
+               config,
+               interfaces,
+       };
+
+       let prev = wdev.handler_data;
+       wdev.handler_data = {};
+
+       if (prev && prev[wdev.name])
+               wdev.handler_data[wdev.name] = prev[wdev.name];
+}
+
+function wdev_setup_cb(wdev)
+{
+       if (wdev.state != "setup")
+               return;
+
+       if (wdev.retry > 0)
+               wdev.retry--;
+       else
+               wdev.retry_setup_failed = true;
+
+       wdev.teardown();
+}
+
+function wdev_teardown_cb(wdev)
+{
+       for (let section, data in wdev.handler_data) {
+               if (data.ifname)
+                       handle_link(data.ifname, data, false);
+       }
+
+       wdev.handler_data = {};
+       wdev.state = "down";
+
+       if (wdev.delete) {
+               delete_wdev(wdev.data.name);
+               return;
+       }
+
+       wdev.setup();
+}
+
+function run_handler_cb(wdev, cb)
+{
+       if (wdev != wdev_cur.wdev)
+               return;
+
+       wdev.dbg("complete " + wdev_cur.op);
+       if (wdev_script_timeout)
+               wdev_script_timeout.cancel();
+       wdev_script_timeout = null;
+       wdev_script_task = null;
+       wdev_cur = null;
+       handler_timer.set(1);
+       cb(wdev);
+}
+
+function run_handler_timeout(wdev, cb)
+{
+       wdev_script_task.cancel();
+       run_handler_cb(wdev, cb);
+}
+
+function handler_sort_fn(a, b)
+{
+       return wdev_handler[a].time - wdev_handler[b].time
+}
+
+function __run_next_handler_name()
+{
+       if (wdev_handler[mlo_name])
+               return mlo_name;
+
+       return sort(keys(wdev_handler), handler_sort_fn)[0];
+}
+
+function __run_next_handler()
+{
+       let name = __run_next_handler_name();
+       if (!name)
+               return;
+
+       wdev_cur = wdev_handler[name];
+       delete wdev_handler[name];
+
+       let wdev = wdev_cur.wdev;
+       let op = wdev_cur.op;
+       let cb = wdev_cur.cb;
+
+       wdev.dbg("run " + op);
+       if (name != mlo_name)
+               wdev_mlo_fixup(wdev.handler_config);
+       wdev.handler_config.data = wdev.handler_data[wdev.name];
+       wdev_script_task = netifd.process({
+               cb: () => run_handler_cb(wdev, cb),
+               dir: netifd.wireless.path,
+               argv: [ './' + wdev.script, wdev.data.config.type, op, wdev.name, "" + wdev.handler_config ],
+               log_prefix: wdev.name,
+       });
+
+       if (!wdev_script_task)
+               return run_handler_cb(wdev, cb);
+
+       wdev_script_timeout = uloop.timer(DEFAULT_SCRIPT_TIMEOUT,
+               () => run_handler_timeout(wdev, cb)
+       );
+}
+
+function run_next_handler()
+{
+       while (!wdev_cur && length(wdev_handler) > 0)
+               __run_next_handler();
+}
+
+function run_handler(wdev, op, cb)
+{
+       wdev.dbg("queue " + op);
+       wdev_handler[wdev.name] = {
+               op, wdev, cb,
+               time: time()
+       };
+
+       run_next_handler();
+}
+
+function wdev_proc_reset(wdev)
+{
+       if (wdev.proc_timer) {
+               wdev.proc_timer.cancel();
+               delete wdev.proc_timer;
+       }
+
+       wdev.procs = [];
+}
+
+function __wdev_proc_check(wdev, proc)
+{
+       if (netifd.process_check(proc.pid, proc.exe))
+               return;
+
+       wdev.dbg(`process ${proc.exe}(${proc.pid}) no longer active`);
+       wdev.teardown();
+       return true;
+}
+
+function wdev_proc_check(wdev)
+{
+       for (let proc in wdev.procs)
+               if (__wdev_proc_check(wdev, proc))
+                       break;
+}
+
+function wdev_proc_add(wdev, data)
+{
+       if (!data.pid || !data.exe)
+               return;
+
+       push(wdev.procs, data);
+
+       if (!wdev.proc_timer)
+               wdev.proc_timer = uloop.interval(1000, () => wdev_proc_check(wdev));
+}
+
+
+function setup()
+{
+       if (this.state != "up" && this.state != "down")
+               return;
+
+       this.dbg("setup, state=" + this.state);
+       if (!this.autostart || this.retry_setup_failed || this.data.config.disabled)
+               return;
+
+       wdev_proc_reset(this);
+       delete this.config_change;
+       this.state = "setup";
+       run_handler(this, "setup", wdev_setup_cb);
+}
+
+function teardown()
+{
+       delete this.cancel_setup;
+
+       this.dbg("teardown, state=" + this.state);
+       if (this.state == "teardown" || this.state == "down")
+               return;
+
+       wdev_proc_reset(this);
+       this.state = "teardown";
+       run_handler(this, "teardown", wdev_teardown_cb);
+}
+
+function wdev_update_disabled_vifs(wdev)
+{
+       let cache = wdev.ifindex_cache;
+       let prev_disabled = wdev.disabled_vifs;
+       let disabled = wdev.disabled_vifs = {};
+       let changed;
+
+       let vifs = [];
+       for (let vif in wdev.data.vif)
+               push(vifs, vif, ...vif.vlan);
+
+       for (let vif in vifs) {
+               let enabled, ifindex;
+
+               for (let net in vif.config.network) {
+                       let state = netifd.interface_get_enabled(net);
+                       if (!state)
+                               continue;
+
+                       if (state.enabled)
+                               enabled = true;
+                       else if (enabled == null)
+                               enabled = false;
+                       if (state.ifindex)
+                               ifindex = state.ifindex;
+               }
+
+               let name = vif.name;
+               if (enabled == false)
+                       disabled[wdev] = true;
+               else if (ifindex != cache[name])
+                       changed = true;
+
+               if (ifindex)
+                       cache[name] = ifindex;
+               else
+                       delete cache[name];
+       }
+
+       if (changed || !is_equal(prev_disabled, disabled))
+               wdev.config_change = true;
+
+       return wdev.config_change;
+}
+
+function wdev_reset(wdev)
+{
+       wdev.retry = DEFAULT_RETRY;
+       delete wdev.retry_setup_failed;
+}
+
+function update(data)
+{
+       if (is_equal(this.data, data))
+               return;
+
+       if (data) {
+               this.data = data;
+               this.ifindex_cache = {};
+               delete this.retry_setup_failed;
+               delete this.delete;
+       }
+
+       wdev_reset(this);
+       this.config_change = true;
+       this.check();
+}
+
+function start()
+{
+       if (this.delete || this.data.config.disabled)
+               return;
+
+       this.dbg("start, state=" + this.state);
+       this.autostart = true;
+       wdev_reset(this);
+
+       if (this.state != "down")
+               return;
+
+       if (wdev_update_disabled_vifs(this))
+               wdev_config_init(this);
+       this.setup();
+}
+
+function stop()
+{
+       this.dbg("stop, state=" + this.state);
+       this.autostart = false;
+
+       switch (this.state) {
+       case "setup":
+               this.cancel_setup = true;
+               break;
+       case "up":
+               this.teardown();
+               break;
+       }
+}
+
+function check()
+{
+       if (!wdev_update_disabled_vifs(this))
+               return;
+
+       wdev_config_init(this);
+       this.setup();
+}
+
+function wdev_mark_up(wdev)
+{
+       wdev.dbg("mark up, state=" + wdev.state);
+       if (wdev.state != "setup")
+               return;
+
+       if (wdev.name == mlo_name)
+               mlo_wdev = wdev;
+
+       if (wdev.config_change) {
+               wdev.setup();
+               return;
+       }
+
+       for (let section, data in wdev.handler_data) {
+               if (data.ifname)
+                       handle_link(data.ifname, data, true);
+       }
+       wdev.state = "up";
+
+       return 0;
+}
+
+function wdev_set_data(wdev, vif, vlan, data)
+{
+       let config = wdev.handler_config;
+       let cur = wdev;
+       let cur_type = "device";
+       if (!config)
+               return libubus.STATUS_INVALID_ARGUMENT;
+
+       if (vif) {
+               cur = vif = config.interfaces[vif];
+               if (!vif)
+                       return libubus.STATUS_NOT_FOUND;
+               cur_type = "vif";
+       }
+
+       if (vlan) {
+               if (!vif)
+                       return libubus.STATUS_INVALID_ARGUMENT;
+
+               cur = vlan = vif.vlans[vlan];
+               if (!vlan)
+                       return libubus.STATUS_NOT_FOUND;
+
+               cur_type = "vlan";
+       }
+
+       wdev.handler_data[cur.name] = {
+               ...cur,
+               ...data,
+               type: cur_type,
+               config: cur.config,
+       };
+
+       return 0;
+}
+
+function notify(req)
+{
+       let vif = req.args.interface;
+       let vlan = req.args.vlan;
+       let data = req.args.data;
+
+       switch (req.args.command) {
+       case NOTIFY_CMD_UP:
+               if (vif || vlan || this.state != "setup")
+                       return libubus.STATUS_INVALID_ARGUMENT;
+
+               return wdev_mark_up(this);
+       case NOTIFY_CMD_SET_DATA:
+               return wdev_set_data(this, vif, vlan, data);
+       case NOTIFY_CMD_PROCESS_ADD:
+               if (this.state != "setup" && this.state != "up")
+                       return 0;
+
+               wdev_proc_add(this, data);
+               return 0;
+       case NOTIFY_CMD_SET_RETRY:
+               if (data.retry != null)
+                       this.retry = data.retry;
+               else
+                       this.retry = DEFAULT_RETRY;
+               return 0;
+       default:
+               return libubus.STATUS_INVALID_ARGUMENT;
+       }
+}
+
+function hotplug(name, add)
+{
+       let dev = name;
+       let m = match(name, /(.+)\.sta.+/);
+       if (m)
+               name = m[1];
+
+       for (let section, data in this.handler_data) {
+               if (data.ifname != name ||
+                   data.type != "vif" && data.type != "vlan")
+                       continue;
+
+               handle_link(dev, data, up);
+       }
+}
+
+function get_status_data(wdev, vif)
+{
+       let hdata = wdev.handler_data[vif.name];
+       let data = {
+               section: vif.name,
+               config: vif.config
+       };
+       if (hdata && hdata.ifname)
+               data.ifname = hdata.ifname;
+       return data;
+}
+
+function get_status_vlans(wdev, vif)
+{
+       let vlans = [];
+       for (let vlan in vif.vlan)
+               push(vlans, get_status_data(wdev, vlan));
+       return vlans;
+}
+
+function get_status_stations(wdev, vif)
+{
+       let vlans = [];
+       for (let vlan in vif.sta)
+               push(vlans, get_status_data(wdev, vlan));
+       return vlans;
+}
+
+function status()
+{
+       let interfaces = [];
+       for (let vif in this.data.vif) {
+               let vlans = get_status_vlans(this, vif);
+               let stations = get_status_stations(this, vif);
+               let data = get_status_data(this, vif);
+               push(interfaces, {
+                       ...data,
+                       vlans, stations
+               });
+       }
+       return {
+               up: this.state == "up",
+               pending: this.state == "setup" || this.state == "teardown",
+               autostart: this.autostart,
+               disabled: !!this.data.config.disabled,
+               retry_setup_failed: !!this.retry_setup_failed,
+               config: this.data.config,
+               interfaces
+       };
+}
+
+function destroy()
+{
+       this.dbg("destroy");
+       this.autostart = false;
+       this.delete = true;
+       if (this.state != "down") {
+               this.stop();
+               return;
+       }
+
+       delete_wdev(this.data.name);
+}
+
+function dbg(msg)
+{
+       netifd.log(netifd.L_DEBUG, `wireless: ${this.name}: ${msg}\n`);
+}
+
+const wdev_proto = {
+       update,
+       destroy,
+       start,
+       stop,
+       setup,
+       status,
+       teardown,
+       check,
+       notify,
+       hotplug,
+       dbg,
+};
+
+export function new(data, script, driver)
+{
+       let wdev = {
+               name: data.name,
+               script, data,
+               procs: [],
+               vifs: {},
+               disabled_vifs: {},
+               ifindex_cache: {},
+
+               autostart: true,
+               state: "down",
+       };
+       wdev_update_disabled_vifs(wdev);
+       wdev_config_init(wdev);
+       handler_timer = uloop.timer(1, run_next_handler);
+       return proto(wdev, wdev_proto);
+};
diff --git a/package/network/config/wifi-scripts/files/lib/netifd/wireless.uc b/package/network/config/wifi-scripts/files/lib/netifd/wireless.uc
new file mode 100644 (file)
index 0000000..de67bd5
--- /dev/null
@@ -0,0 +1,450 @@
+'use strict';
+
+import * as libubus from "ubus";
+import { realpath } from "fs";
+import {
+       handler_load, handler_attributes,
+       parse_attribute_list, parse_bool, parse_array,
+       TYPE_ARRAY, TYPE_STRING, TYPE_INT, TYPE_BOOL
+} from "./utils.uc";
+import * as wdev from "./wireless-device.uc";
+
+let ubus = netifd.ubus;
+let wireless = netifd.wireless = {
+       handlers: {},
+       devices: {},
+       path: realpath(netifd.main_path + "/wireless"),
+};
+
+function update_config(new_devices)
+{
+       for (let name, dev in wireless.devices)
+               if (!new_devices[name])
+                       dev.destroy();
+
+       for (let name, dev in new_devices) {
+               let cur_dev = wireless.devices[name];
+               if (cur_dev) {
+                       cur_dev.update(dev);
+                       continue;
+               }
+
+               let handler = wireless.handlers[dev.config.type];
+               cur_dev = wdev.new(dev, handler.script);
+               if (!cur_dev)
+                       continue;
+
+               wireless.devices[name] = cur_dev;
+       }
+}
+
+function config_init(uci)
+{
+       let config = uci.get_all("wireless");
+
+       let handlers = {};
+       let devices = {};
+       let vifs = {};
+       let mlo_device;
+
+       let sections = {
+               device: {},
+               iface: {},
+               vlan: {},
+               station: {},
+       };
+       let radio_idx = {};
+
+       for (let name, data in config) {
+               let type = data[".type"];
+               if (parse_bool(data.disabled) && type != "wifi-device")
+                       continue;
+
+               if (substr(type, 0, 5) != "wifi-")
+                       continue;
+
+               let list = sections[substr(type, 5)];
+               if (list)
+                       list[name] = data;
+
+               if (type == "wifi-iface" && parse_bool(data.mlo))
+                       mlo_device = true;
+       }
+
+       if (mlo_device) {
+               devices[wdev.mlo_name] = {
+                       name: wdev.mlo_name,
+                       config: {
+                               type: "mac80211",
+                       },
+                       vif: [],
+               };
+               handlers[wdev.mlo_name] = wireless.handlers.mac80211;
+       }
+
+       for (let name, data in sections.device) {
+               if (!data.type)
+                       continue;
+
+               let handler = wireless.handlers[data.type];
+               if (!handler)
+                       continue;
+
+               if (data.radio != null)
+                       radio_idx[name] = +data.radio;
+
+               let config = parse_attribute_list(data, handler.device);
+               devices[name] = {
+                       name,
+                       config,
+
+                       vif: [],
+               };
+               handlers[name] = handler;
+       }
+
+       for (let name, data in sections.iface) {
+               let dev_names = parse_array(data.device);
+               let mlo_vif = parse_bool(data.mlo);
+               let radios = map(dev_names, (v) => radio_idx[v]);
+               radios = filter(radios, (v) => v != null);
+               if (mlo_vif)
+                       dev_names = [ wdev.mlo_name, ...dev_names ];
+               for (let dev_name in dev_names) {
+                       let dev = devices[dev_name];
+                       if (!dev)
+                               continue;
+
+                       let handler = handlers[dev_name];
+                       if (!handler)
+                               continue;
+
+                       let config = parse_attribute_list(data, handler.iface);
+                       if (mlo_vif && dev_name != wdev.mlo_name)
+                               config.mode = "link";
+                       config.radios = radios;
+
+                       let vif = {
+                               name, config,
+                               device: dev_name,
+
+                               vlan: [],
+                               sta: [],
+                       };
+                       push(dev.vif, vif);
+
+                       vifs[name] ??= [];
+                       push(vifs[name], vif);
+               }
+       }
+
+       for (let name, data in sections.vlan) {
+               if (!data.iface || !vifs[data.iface])
+                       continue;
+
+               for (let vif in vifs[data.iface]) {
+                       let dev = devices[vif.device];
+                       let handler = handlers[vif.device];
+                       if (!dev || !handler)
+                               continue;
+
+                       let config = parse_attribute_list(data, handler.vlan);
+
+                       let vlan = {
+                               name,
+                               config
+                       };
+                       push(vif.vlan, vlan);
+               }
+       }
+
+       for (let name, data in sections.station) {
+               if (!data.iface || !vifs[data.iface])
+                       continue;
+
+               for (let vif in vifs[data.iface]) {
+                       let dev = devices[vif.device];
+                       let handler = handlers[vif.device];
+                       if (!dev || !handler)
+                               continue;
+
+                       let config = parse_attribute_list(data, handler.station);
+
+                       let sta = {
+                               name,
+                               config
+                       };
+                       push(vif.sta, sta);
+               }
+       }
+
+       let udata = ubus.call({
+               object: "service",
+               method: "get_data",
+               data: {
+                       type: "wifi-iface"
+               },
+       });
+
+       for (let svcname, svc in udata) {
+               for (let typename, data in svc) {
+                       for (let radio, vifs in data) {
+                               for (let name, vif in vifs) {
+                                       let devs = vif.device;
+                                       if (type(devs) != "array")
+                                               devs = [ devs ];
+                                       let config = vif.config;
+                                       if (!config)
+                                               continue;
+                                       for (let device in devs) {
+                                               let dev = devices[device];
+                                               if (!dev)
+                                                       continue;
+
+                                               let vif_data = {
+                                                       name, device, config,
+                                                       vlan: [],
+                                                       sta: []
+                                               };
+                                               if (vif.vlans)
+                                                       vif_data.vlans = vif.vlans;
+                                               if (vif.stations)
+                                                       vif_data.sta = vif.stations;
+                                               vifs[name] ??= [];
+                                               push(vifs[name], vif_data);
+                                               push(dev.vif, vif_data);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       update_config(devices);
+}
+
+function config_start()
+{
+       for (let name, dev in wireless.devices)
+               if (dev.autostart)
+                       dev.start();
+
+}
+
+function check_interfaces()
+{
+       for (let name, dev in wireless.devices)
+               if (dev.autostart)
+                       dev.check();
+}
+
+function hotplug(name, add)
+{
+       for (let name, dev in wireless.devices)
+               if (dev.autostart)
+                       dev.hotplug(name, add);
+}
+
+const network_config_attr = {
+       network: TYPE_ARRAY,
+       network_vlan: TYPE_ARRAY,
+       bridge_isolate: TYPE_BOOL,
+       isolate: TYPE_BOOL,
+       proxy_arp: TYPE_BOOL,
+       multicast_to_unicast: TYPE_BOOL,
+};
+
+const default_config_attr = {
+       device: {
+               disabled: TYPE_BOOL,
+               type: TYPE_STRING,
+       },
+       iface: {
+               ...network_config_attr,
+               device: TYPE_STRING,
+               mode: TYPE_STRING,
+       },
+       station: {
+               iface: TYPE_STRING,
+
+               mac: TYPE_STRING,
+               key: TYPE_STRING,
+               vid: TYPE_STRING,
+       },
+       vlan: {
+               ...network_config_attr,
+               iface: TYPE_STRING,
+               name: TYPE_STRING,
+               vid: TYPE_STRING,
+       },
+};
+
+const wdev_args = {
+       device: ""
+};
+
+function wdev_call(req, cb)
+{
+       let dev = req.args.device;
+       if (dev) {
+               dev = wireless.devices[dev];
+               if (!dev)
+                       return libubus.STATUS_NOT_FOUND;
+
+               return cb(dev);
+       }
+
+       for (let name, dev in wireless.devices) {
+               if (name == wdev.mlo_name)
+                       continue;
+               cb(dev);
+       }
+
+       return 0;
+}
+
+function attr_validate(attr_type, validate)
+{
+       if (validate)
+               return validate;
+       switch (attr_type) {
+       case TYPE_STRING:
+               return "string";
+       case TYPE_ARRAY:
+               return "list(string)";
+       case TYPE_INT:
+               return "uinteger";
+       case TYPE_BOOL:
+               return "bool";
+       }
+}
+
+function get_validate_info(ret, handler)
+{
+       for (let kind in default_config_attr) {
+               let cur = ret[kind == "iface" ? "interface" : kind] = {};
+               let validate = handler[kind + "_validate"];
+
+               for (let attr, attr_type in handler[kind]) {
+                       let val = attr_validate(attr_type, validate[attr]);
+                       if (val != null)
+                               cur[attr] = val;
+               }
+       }
+
+       return ret;
+}
+
+const ubus_obj = {
+       up: {
+               args: wdev_args,
+               call: function(req) {
+                       let mlo_dev = wireless.devices[wdev.mlo_name];
+                       if (mlo_dev)
+                               mlo_dev.start();
+
+                       return wdev_call(req, (dev) => {
+                               dev.start();
+                               return 0;
+                       });
+               }
+       },
+       down: {
+               args: wdev_args,
+               call: function(req) {
+                       let mlo_dev = wireless.devices[wdev.mlo_name];
+                       if (mlo_dev)
+                               mlo_dev.config_change = true;
+
+                       return wdev_call(req, (dev) => {
+                               dev.stop();
+                               return 0;
+                       });
+               }
+       },
+       reconf: {
+               args: wdev_args,
+               call: function(req) {
+                       let mlo_dev = wireless.devices[wdev.mlo_name];
+                       if (mlo_dev)
+                               mlo_dev.update();
+
+                       return wdev_call(req, (dev) => {
+                               dev.update();
+                               return 0;
+                       });
+               }
+       },
+       status: {
+               args: wdev_args,
+               call: function(req) {
+                       let ret = {};
+                       let err = wdev_call(req, (dev) => {
+                               ret[dev.data.name] = dev.status();
+                               return 0;
+                       });
+                       if (err != 0)
+                               return err;
+
+                       return ret;
+               }
+       },
+       notify: {
+               args: {
+                       ...wdev_args,
+                       command: 0,
+                       interface: "",
+                       vlan: "",
+                       data: {},
+               },
+               call: function(req) {
+                       let dev = req.args.device;
+                       if (!dev)
+                               return libubus.STATUS_INVALID_ARGUMENT;
+
+                       dev = wireless.devices[dev];
+                       if (!dev)
+                               return libubus.STATUS_NOT_FOUND;
+
+                       return dev.notify(req);
+               }
+       },
+       get_validate: {
+               args: wdev_args,
+               call: function(req) {
+                       let ret = {};
+                       let err = wdev_call(req, (dev) => {
+                               let dev_type = dev.data.config.type;
+                               let cur = ret[dev.data.name] = {};
+                               get_validate_info(cur, wireless.handlers[dev_type]);
+                               return 0;
+                       });
+                       if (err != 0)
+                               return err;
+
+                       return ret;
+               }
+       },
+};
+
+
+handler_load(wireless.path, (script, data) => {
+       if (!data.name)
+               return;
+
+       let handler = wireless.handlers[data.name] = {
+               script,
+       };
+       for (let kind, attr in default_config_attr) {
+               let validate = handler[kind + "_validate"] = {};
+               handler[kind] = handler_attributes(data[kind], attr, validate);
+       }
+});
+
+wireless.obj = ubus.publish("network.wireless", ubus_obj);
+
+return {
+       hotplug,
+       config_init,
+       config_start,
+       check_interfaces,
+};