move plugins to their own folder
authorJohn Crispin <[email protected]>
Mon, 26 May 2025 09:12:06 +0000 (11:12 +0200)
committerJohn Crispin <[email protected]>
Mon, 26 May 2025 10:30:33 +0000 (12:30 +0200)
Signed-off-by: John Crispin <[email protected]>
files/usr/share/ufp/plugin_dhcp.uc [deleted file]
files/usr/share/ufp/plugin_mdns.uc [deleted file]
files/usr/share/ufp/plugin_wifi.uc [deleted file]
plugins/plugin_dhcp.uc [new file with mode: 0644]
plugins/plugin_mdns.uc [new file with mode: 0644]
plugins/plugin_wifi.uc [new file with mode: 0644]

diff --git a/files/usr/share/ufp/plugin_dhcp.uc b/files/usr/share/ufp/plugin_dhcp.uc
deleted file mode 100644 (file)
index d34a582..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-let ubus, sub, listener, global;
-
-const dhcp_parser_proto = {
-       reset: function() {
-               this.offset = 0;
-       },
-
-       parseAt: function(offset) {
-               let id = hex(substr(this.buffer, offset, 2));
-               let len = hex(substr(this.buffer, offset + 2, 2)) * 2;
-               if (type(id) != "int" || type(len) != "int")
-                       return null;
-
-               let data = substr(this.buffer, offset + 4, len);
-               if (length(data) != len)
-                       return null;
-
-               return [ id, data, len + 4 ];
-       },
-
-       next: function() {
-               let data = this.parseAt(this.offset);
-               if (!data)
-                       return null;
-
-               this.offset += data[2];
-               return data;
-       },
-
-       foreach: function(cb) {
-               let offset = 0;
-               let data;
-
-               while ((data = this.parseAt(offset)) != null) {
-                       offset += data[2];
-                       let ret = cb(data);
-                       if (type(ret) == "boolean" && !ret)
-                               break;
-               }
-       },
-};
-
-function dhcp_opt_parser(data) {
-       let parser = {
-               offset: 0,
-               buffer: data,
-       };
-
-       proto(parser, dhcp_parser_proto);
-
-       return parser;
-}
-
-function parse_array(data)
-{
-       return map(match(data, /../g), (val) => val[0]);
-}
-
-function parse_string(data)
-{
-       return join("", map(parse_array(data[1]), (val) => chr(hex(val))));
-}
-
-function parse_macaddr(addr)
-{
-       return join(":", parse_array(addr));
-}
-
-function dhcp_cb(msg) {
-       if (msg.type != "discover" && msg.type != "request")
-               return;
-
-       let packet = msg.data.packet;
-       if (!packet)
-               return;
-
-       let opts = substr(packet, 240 * 2);
-       if (length(opts) < 16)
-               return;
-
-       let macaddr = parse_macaddr(substr(packet, 28 * 2, 12));
-
-       opts = dhcp_opt_parser(opts);
-       opts.foreach((data) => {
-               let id = data[0];
-               switch (id) {
-               case 12:
-                       typestr = "%device_name|dhcp_device_name";
-                       data = parse_string(data);
-                       break;
-               case 55:
-                       typestr = "dhcp_req";
-                       data = join(",", map(parse_array(data[1]), (val) => hex(val)));
-                       break;
-               case 60:
-                       typestr = "dhcp_vendorid";
-                       data = parse_string(data);
-                       break;
-               default:
-                       return;
-               }
-               global.device_add_data(macaddr, `${typestr}|${data}`);
-       });
-}
-
-function init(gl) {
-       global = gl;
-       ubus = gl.ubus;
-
-       gl.weight.dhcp_req = 1.1;
-       gl.weight.dhcp_device_name = 5.0;
-       sub = ubus.subscriber(dhcp_cb);
-       listener = ubus.listener("ubus.object.add", (event, msg) => {
-               if (msg.path == "dhcpsnoop")
-                       sub.subscribe(msg.path);
-       });
-       sub.subscribe("dhcpsnoop");
-}
-
-return { init };
diff --git a/files/usr/share/ufp/plugin_mdns.uc b/files/usr/share/ufp/plugin_mdns.uc
deleted file mode 100644 (file)
index 308d379..0000000
+++ /dev/null
@@ -1,307 +0,0 @@
-let ubus, global, last_refresh;
-let rtnl = require("rtnl");
-
-const homekit_types = {
-       "2": "Bridge",
-       "3": "Fan",
-       "4": "Garage Door Opener",
-       "5": "Lightbulb",
-       "6": "Door Lock",
-       "7": "Outlet",
-       "8": "Switch",
-       "9": "Thermostat",
-       "10": "Sensor",
-       "11": "Security System",
-       "12": "Door",
-       "13": "Window",
-       "14": "Window Covering",
-       "15": "Programmable Switch",
-       "17": "IP Camera",
-       "18": "Video Doorbell",
-       "19": "Air Purifier",
-       "20": "Heater",
-       "21": "Air Conditioner",
-       "22": "Humidifier",
-       "23": "Dehumidifier",
-       "28": "Sprinkler",
-       "29": "Faucet",
-       "30": "Shower System",
-       "31": "Television",
-       "32": "Remote Control",
-       "33": "WiFi Router",
-       "34": "Audio Receiver",
-       "35": "TV Set Top Box",
-       "36": "TV Stick",
-};
-
-function get_arp_cache() {
-       let ret = {};
-
-       let cache = rtnl.request(rtnl.const.RTM_GETNEIGH, rtnl.const.NLM_F_DUMP, {});
-       for (let data in cache) {
-               if (!data.lladdr || !data.dst)
-                       continue;
-
-               ret[data.dst] = data.lladdr;
-       }
-
-       return ret;
-}
-
-function find_arp_entry(arp, addrs)
-{
-       for (let addr in addrs) {
-               let val = arp[addr];
-               if (val)
-                       return val;
-       }
-       return null;
-}
-
-function get_host_addr_cache()
-{
-       let arp = get_arp_cache();
-       let host_cache = ubus.call("umdns", "hosts", { array: true });
-       let host_addr = {};
-       for (let host in host_cache) {
-               let host_data = host_cache[host];
-               host_addr[host] = find_arp_entry(arp, host_data.ipv4) ??
-                                 find_arp_entry(arp, host_data.ipv6);
-       }
-
-       return host_addr;
-}
-
-function convert_txt_array(data) {
-       let txt = {};
-
-       for (let rec in data) {
-               rec = split(rec, "=", 2);
-               if (rec[1])
-                       txt[rec[0]] = rec[1];
-       }
-
-       return txt;
-}
-
-function handle_apple(txt, name)
-{
-       let ret = [];
-       let model = txt.model ?? txt.rpMd ?? txt.am;
-       if (model)
-               push(ret, `apple_model|${model}`);
-       if (name)
-               push(ret, `%device_name|mdns_implicit_device_name|${name}`);
-
-       return ret;
-}
-
-function handle_homekit(txt)
-{
-       let ret = [];
-       if (txt.ci) {
-               let type = homekit_types[txt.ci];
-               if (type)
-                       push(ret, `%class|homekit_class|${type}`);
-       }
-
-       if (txt.md)
-               push(ret, `%device|mdns_model_string|${txt.md}`);
-
-       return ret;
-}
-
-function handle_googlecast_model(txt)
-{
-       let ret = [];
-       let model = txt.model ?? txt.rpMd ?? txt.md;
-       if (model)
-               push(ret, `%device|mdns_model_string|${model}`);
-       if (txt.fn)
-               push(ret, `%device_name|mdns_device_name|${txt.fn}`);
-       if (txt.rs == 'TV')
-               push(ret, "%class|mdns_tv|TV");
-       return ret;
-}
-
-function handle_printer(txt)
-{
-       let ret = [];
-       let vendor = txt.usb_MFG;
-       let model = txt.usb_MDL ?? txt.ty ?? replace(txt.product, /^\((.*)\)$/, "$1");
-       let weight = (txt.usb_MFG && txt.usb_MDL) ? "mdns_vendor_model_string" : "mdns_model_string";
-       if (vendor)
-               push(ret, `%vendor|${weight}|${vendor}`);
-       if (model)
-               push(ret, `%device|${weight}|${model}`);
-       push(ret, "%class|mdns_printer|Printer");
-
-       return ret;
-}
-
-function handle_scanner(txt)
-{
-       let ret = [];
-       let vendor = txt.mfg;
-       let model = txt.mdl ?? txt.ty;
-       let weight = (txt.mfg && txt.mdl) ? "mdns_vendor_model_string" : "mdns_model_string";
-       if (vendor)
-               push(ret, `%vendor|${weight}|${vendor}`);
-       if (model)
-               push(ret, `%device|${weight}|${model}`);
-       push(ret, "%class|mdns_scanner|Scanner");
-
-       return ret;
-}
-
-function handle_hue(txt, name)
-{
-       let ret = [];
-       push(ret, `%vendor|mdns_service|Philips`);
-       push(ret, `%device|mdns_service|Hue`);
-       if (name)
-               push(ret, `%device_name|mdns_implicit_device_name|${name}`);
-
-       return ret;
-}
-
-function handle_fritzbox(txt)
-{
-       let ret = [];
-       push(ret, `%vendor|mdns_service|AVM`);
-       push(ret, `%device|mdns_service|FRITZ!Box`);
-
-       return ret;
-}
-
-function handle_uconfig(txt)
-{
-       let ret = [];
-       push(ret, "%class|mdns_uconfig|uconfig");
-       return ret;
-}
-
-const service_handler = {
-       "_airplay._tcp": handle_apple,
-       "_companion-link._tcp": handle_apple,
-       "_raop._tcp": (txt) => handle_apple(txt), // skip name
-       "_googlecast._tcp": handle_googlecast_model,
-       "_ipp._tcp": handle_printer,
-       "_scanner._tcp": handle_scanner,
-       "_hap._tcp": handle_homekit,
-       "_hap._udp": handle_homekit,
-       "_hue._tcp": handle_hue,
-       "_fbox._tcp": handle_fritzbox,
-       "_avmnexus._tcp": handle_fritzbox,
-       "_uconfig._udp": handle_uconfig,
-};
-
-function arp_resolve(list)
-{
-       let host_cache = ubus.call("umdns", "hosts", { array: true });
-       for (let entry in list) {
-               let iface = entry[0];
-               let host = entry[1];
-               if (!host_cache[host])
-                       continue;
-               for (let addr in host_cache[host].ipv4)
-                       rtnl.request(rtnl.const.RTM_NEWNEIGH,
-                                    rtnl.const.NLM_F_CREATE | rtnl.const.NLM_F_REPLACE,
-                                    { dev: iface, state: rtnl.const.NUD_INCOMPLETE,
-                                      flags: rtnl.const.NTF_USE, family: rtnl.const.AF_INET,
-                                      dst: addr });
-       }
-}
-
-function refresh() {
-       if (last_refresh == time())
-               return;
-
-       let host_cache = get_host_addr_cache();
-       let query_list = [];
-       let arp_pending = [];
-       let mdns_data;
-
-       for (let i = 0; i < 2; i++) {
-               mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
-               for (let service in mdns_data) {
-                       if (!service_handler[service])
-                               continue;
-                       let service_data = mdns_data[service];
-                       for (let host in service_data) {
-                               let host_entry = service_data[host].host;
-                               let iface = service_data[host].iface;
-                               if (!iface)
-                                       continue;
-                               if (!host_entry)
-                                       push(query_list, [ `${host}.${service}.local`, iface ]);
-                               else if (!host_cache[host_entry])
-                                       push(arp_pending, [ iface, host_entry ]);
-                       }
-               }
-
-               if (!length(arp_pending) && !length(query_list))
-                       break;
-
-               if (length(arp_pending) > 0)
-                       arp_resolve(arp_pending);
-
-               if (length(query_list) > 0) {
-                       for (let query in query_list)
-                               ubus.call("umdns", "query", { question: query[0], interface: query[1] });
-               }
-
-               sleep(1000);
-
-               host_cache = get_host_addr_cache();
-               mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
-       }
-
-       for (let service in mdns_data) {
-               if (!service_handler[service])
-                       continue;
-
-               let service_data = mdns_data[service];
-               for (let host in service_data) {
-                       let txt = service_data[host].txt;
-                       if (!txt)
-                               continue;
-
-                       let host_entry = service_data[host].host;
-                       if (!host_entry)
-                               continue;
-
-                       let mac = host_cache[host_entry];
-                       if (!mac)
-                               continue;
-
-                       txt = convert_txt_array(txt);
-                       let match = service_handler[service](txt, host);
-                       for (let info in match)
-                               global.device_add_data(mac, info);
-
-                       global.device_refresh(mac);
-               }
-       }
-}
-
-function init(gl) {
-       global = gl;
-       ubus = gl.ubus;
-
-       global.add_weight({
-               apple_model: 10.0,
-               mdns_device_name: 10.0,
-               mdns_implicit_device_name: 5.0,
-               mdns_vendor_model_string: 10.0,
-               mdns_service: 10.0,
-               mdns_tv: 5.0,
-               mdns_model_string: 5.0,
-               mdns_printer: 5.0,
-               mdns_scanner: 1.0,
-               homekit_class: 2.0,
-               mdns_uconfig: 10.0,
-       });
-}
-
-return { init, refresh };
diff --git a/files/usr/share/ufp/plugin_wifi.uc b/files/usr/share/ufp/plugin_wifi.uc
deleted file mode 100644 (file)
index 693d3c0..0000000
+++ /dev/null
@@ -1,320 +0,0 @@
-import * as struct from "struct";
-let ubus, uloop, global, timer;
-let ap_cache = {};
-
-const ie_tags = {
-       PWR_CAPABILITY: 33,
-       HT_CAP: 45,
-       EXT_CAPAB: 127,
-       VHT_CAP: 191,
-       __EXT_START: 0x100,
-       HE_CAP: 0x100 | 35,
-       VENDOR_WPS: 0x0050f204,
-};
-
-const ie_parser_proto = {
-       reset: function() {
-               this.offset = 0;
-       },
-
-       parseAt: function(offset) {
-               let hdr = substr(this.buffer, offset, 2);
-               if (length(hdr) != 2)
-                       return null;
-
-               let data = this.hdr.unpack(hdr);
-               if (length(data != 2))
-                       return null;
-
-               let len = data[1];
-               offset += 2;
-               data[1] += 2;
-
-               if (data[0] == 221 && len >= 4) {
-                       hdr = substr(this.buffer, offset, 4);
-                       if (length(hdr) != 4)
-                               return null;
-
-                       let val = this.vendor_hdr.unpack(hdr);
-                       if (length(val) != 1 || val[0] < 0x200)
-                               return null;
-
-                       data[0] = val[0];
-                       len -= 4;
-                       offset += 4;
-               } else if (data[0] == 255 && len >= 1) {
-                       hdr = substr(this.buffer, offset, 2);
-                       if (length(hdr) != 2)
-                               return null;
-                       data[0] = 0x100 + this.hdr.unpack(hdr)[0];
-                       len -= 1;
-                       offset += 1;
-               }
-
-               data[2] = data[1];
-               data[1] = substr(this.buffer, offset, len);
-               if (length(data[1]) != len)
-                       return null;
-
-               return data;
-       },
-
-       next: function() {
-               let data = this.parseAt(this.offset);
-               if (!data)
-                       return null;
-
-               this.offset += data[2];
-               return data;
-       },
-
-       foreach: function(cb) {
-               let offset = 0;
-               let data;
-
-               while ((data = this.parseAt(offset)) != null) {
-                       offset += data[2];
-                       let ret = cb(data);
-                       if (type(ret) == "boolean" && !ret)
-                               break;
-               }
-       },
-};
-
-function ie_parser(data) {
-       let parser = {
-               offset: 0,
-               buffer: data,
-               hdr: struct.new("BB"),
-               vendor_hdr: struct.new(">I"),
-       };
-
-       proto(parser, ie_parser_proto);
-
-       return parser;
-}
-
-function format_fn(unpack_str, format)
-{
-       return (data) => {
-               data = struct.unpack(unpack_str, data);
-               if (data && data[0])
-                       data = data[0];
-               else
-                       data = 0;
-               return sprintf(format, data)
-       };
-}
-
-let unpack;
-unpack = {
-       u8: format_fn("B", "%02x"),
-       le16: format_fn("<H", "%04x"),
-       le32: format_fn("<I", "%08x"),
-       bytes: (data) => join("", map(split(data, ""), unpack.u8)),
-};
-let fingerprint_order = [
-       "htcap", "htagg", "htmcs", "vhtcap", "vhtrxmcs", "vhttxmcs",
-       "txpow", "extcap", "wps", "hemac", "hephy"
-];
-
-function format_wps_ie(data) {
-       let offset = 0;
-       let len = length(data);
-       let s = struct.new("<HH");
-
-       while (offset + 4 <= len) {
-               let hdr = s.unpack(substr(data, offset, 4));
-               let val = substr(data, offset + 4, hdr[1]);
-
-               offset += 4 + hdr[1];
-               if (hdr[0] != 0x1023)
-                       continue;
-
-               if (length(val) != hdr[1])
-                       break;
-
-               return replace(val, /[^A-Za-z0-9]/, "_");
-       }
-
-       return null;
-}
-
-function ie_fingerprint_str(id) {
-       if (id >= 0x200)
-               return sprintf("221(%08x)", id);
-       if (id >= 0x100)
-               return sprintf("255(%d)", id - 0x100);
-       return sprintf("%d", id);
-}
-
-let vendor_ie_filter = [
-       0x0050f2, // Microsoft WNN
-       0x506f9a, // WBA
-       0x8cfdf0, // Qualcom
-       0x001018, // Broadcom
-       0x000c43, // Ralink
-];
-
-function ie_fingerprint(data, mode) {
-       let caps = {
-               tags: [],
-               vendor_list: {}
-       };
-       let parser = ie_parser(data);
-
-       parser.foreach(function(ie) {
-               let skip = false;
-               let val = ie[1];
-               switch (ie[0]) {
-               case ie_tags.HT_CAP:
-                       caps.htcap = unpack.le16(substr(val, 0, 2));
-                       caps.htagg = unpack.u8(substr(val, 2, 1));
-                       caps.htmcs = unpack.le32(substr(val, 3, 4));
-                       break;
-               case ie_tags.VHT_CAP:
-                       caps.vhtcap = unpack.le32(substr(val, 0, 4));
-                       caps.vhtrxmcs = unpack.le32(substr(val, 4, 4));
-                       caps.vhttxmcs = unpack.le32(substr(val, 8, 4));
-                       break;
-               case ie_tags.EXT_CAPAB:
-                       caps.extcap = unpack.bytes(val);
-                       break;
-               case ie_tags.PWR_CAPABILITY:
-                       caps.txpow = unpack.le16(val);
-                       break;
-               case ie_tags.VENDOR_WPS:
-                       caps.wps = format_wps_ie(val);
-                       break;
-               case ie_tags.HE_CAP:
-                       if (mode != "wifi6") {
-                               skip = true;
-                               break;
-                       }
-                       caps.hemac =
-                               unpack.le16(substr(val, 4, 2)) +
-                               unpack.le32(substr(val, 0, 4));
-                       caps.hephy =
-                               unpack.le16(substr(val, 15, 2)) +
-                               unpack.le32(substr(val, 11, 4)) +
-                               unpack.le32(substr(val, 7, 4));
-                       break;
-               }
-               if (ie[0] > 0x200) {
-                       let vendor = ie[0] >> 8;
-                       if (!(vendor in vendor_ie_filter))
-                               caps.vendor_list[sprintf("%06x", vendor)] = 1;
-               }
-               if (!skip)
-                       push(caps.tags, ie[0]);
-               return null;
-       });
-
-       switch (mode) {
-       case "wifi6":
-               if (mode == "wifi6" && !caps.hemac)
-                       return null;
-               break;
-       case "wifi-vendor-oui":
-               return caps.vendor_list;
-       default:
-               break;
-       }
-
-       let tags = map(caps.tags, ie_fingerprint_str);
-       return
-               join(",", tags) + "," +
-               join(",", map(
-                       filter(fingerprint_order, (key) => !!caps[key]),
-                       (key) => `${key}:${caps[key]}`
-               ));
-}
-
-function fingerprint(mac, mode, ies) {
-       switch (mode) {
-       case "wifi4":
-               if (!ies.assoc_ie)
-                       break;
-
-               let assoc = ie_fingerprint(ies.assoc_ie, mode);
-               if (!assoc)
-                       break;
-
-               global.device_add_data(mac, `${mode}|${assoc}`);
-               break;
-       case "wifi-vendor-oui":
-               let list = ie_fingerprint(ies.assoc_ie, mode);
-               for (let oui in list) {
-                       global.device_add_data(mac, `${mode}-${oui}|1`);
-               }
-               break;
-       case "wifi6":
-       default:
-               let val = ie_fingerprint(ies.assoc_ie, mode);
-               if (!val)
-                       break;
-
-               global.device_add_data(mac, `${mode}|${val}`);
-               break;
-       }
-}
-
-const fingerprint_modes = [ "wifi4", "wifi6", "wifi-vendor-oui" ];
-
-function client_refresh(ap, mac, prev_cache)
-{
-       let ies = ubus.call(ap, "get_sta_ies", { address: mac });
-       if (type(ies) != "object" || !ies.assoc_ie)
-               return null;
-
-       ies.assoc_ie = b64dec(ies.assoc_ie);
-       if (ies.probe_ie)
-               ies.probe_ie = b64dec(ies.probe_ie);
-
-       for (let mode in fingerprint_modes)
-               fingerprint(mac, mode, ies);
-
-       return ies;
-}
-
-function refresh()
-{
-       let ap_objs = filter(ubus.list(), (name) => match(name, /^hostapd\./));
-       let prev_cache = ap_cache;
-       ap_cache = {};
-
-       timer.set(30 * 1000);
-       for (let ap in ap_objs) {
-               try {
-                       let cur_cache = {};
-                       let prev_ap_cache = prev_cache[ap] ?? {};
-
-                       ap_cache[ap] = cur_cache;
-
-                       let clients = ubus.call(ap, "get_clients").clients;
-                       for (let client in clients) {
-                               let client_cache = prev_ap_cache[client];
-                               if (!client_cache || !client_cache.assoc_ie || !client_cache.probe_ie)
-                                       client_cache = client_refresh(ap, client);
-                               global.device_refresh(client);
-                       }
-               } catch (e) {
-               }
-       }
-}
-
-function init(gl) {
-       global = gl;
-       ubus = gl.ubus;
-       uloop = gl.uloop;
-
-       global.add_weight({
-               wifi4: 2.0,
-               wifi6: 3.0,
-               "wifi-vendor-oui": 2.0
-       });
-
-       timer = uloop.timer(1000, refresh);
-}
-
-return { init, refresh };
diff --git a/plugins/plugin_dhcp.uc b/plugins/plugin_dhcp.uc
new file mode 100644 (file)
index 0000000..d34a582
--- /dev/null
@@ -0,0 +1,120 @@
+let ubus, sub, listener, global;
+
+const dhcp_parser_proto = {
+       reset: function() {
+               this.offset = 0;
+       },
+
+       parseAt: function(offset) {
+               let id = hex(substr(this.buffer, offset, 2));
+               let len = hex(substr(this.buffer, offset + 2, 2)) * 2;
+               if (type(id) != "int" || type(len) != "int")
+                       return null;
+
+               let data = substr(this.buffer, offset + 4, len);
+               if (length(data) != len)
+                       return null;
+
+               return [ id, data, len + 4 ];
+       },
+
+       next: function() {
+               let data = this.parseAt(this.offset);
+               if (!data)
+                       return null;
+
+               this.offset += data[2];
+               return data;
+       },
+
+       foreach: function(cb) {
+               let offset = 0;
+               let data;
+
+               while ((data = this.parseAt(offset)) != null) {
+                       offset += data[2];
+                       let ret = cb(data);
+                       if (type(ret) == "boolean" && !ret)
+                               break;
+               }
+       },
+};
+
+function dhcp_opt_parser(data) {
+       let parser = {
+               offset: 0,
+               buffer: data,
+       };
+
+       proto(parser, dhcp_parser_proto);
+
+       return parser;
+}
+
+function parse_array(data)
+{
+       return map(match(data, /../g), (val) => val[0]);
+}
+
+function parse_string(data)
+{
+       return join("", map(parse_array(data[1]), (val) => chr(hex(val))));
+}
+
+function parse_macaddr(addr)
+{
+       return join(":", parse_array(addr));
+}
+
+function dhcp_cb(msg) {
+       if (msg.type != "discover" && msg.type != "request")
+               return;
+
+       let packet = msg.data.packet;
+       if (!packet)
+               return;
+
+       let opts = substr(packet, 240 * 2);
+       if (length(opts) < 16)
+               return;
+
+       let macaddr = parse_macaddr(substr(packet, 28 * 2, 12));
+
+       opts = dhcp_opt_parser(opts);
+       opts.foreach((data) => {
+               let id = data[0];
+               switch (id) {
+               case 12:
+                       typestr = "%device_name|dhcp_device_name";
+                       data = parse_string(data);
+                       break;
+               case 55:
+                       typestr = "dhcp_req";
+                       data = join(",", map(parse_array(data[1]), (val) => hex(val)));
+                       break;
+               case 60:
+                       typestr = "dhcp_vendorid";
+                       data = parse_string(data);
+                       break;
+               default:
+                       return;
+               }
+               global.device_add_data(macaddr, `${typestr}|${data}`);
+       });
+}
+
+function init(gl) {
+       global = gl;
+       ubus = gl.ubus;
+
+       gl.weight.dhcp_req = 1.1;
+       gl.weight.dhcp_device_name = 5.0;
+       sub = ubus.subscriber(dhcp_cb);
+       listener = ubus.listener("ubus.object.add", (event, msg) => {
+               if (msg.path == "dhcpsnoop")
+                       sub.subscribe(msg.path);
+       });
+       sub.subscribe("dhcpsnoop");
+}
+
+return { init };
diff --git a/plugins/plugin_mdns.uc b/plugins/plugin_mdns.uc
new file mode 100644 (file)
index 0000000..308d379
--- /dev/null
@@ -0,0 +1,307 @@
+let ubus, global, last_refresh;
+let rtnl = require("rtnl");
+
+const homekit_types = {
+       "2": "Bridge",
+       "3": "Fan",
+       "4": "Garage Door Opener",
+       "5": "Lightbulb",
+       "6": "Door Lock",
+       "7": "Outlet",
+       "8": "Switch",
+       "9": "Thermostat",
+       "10": "Sensor",
+       "11": "Security System",
+       "12": "Door",
+       "13": "Window",
+       "14": "Window Covering",
+       "15": "Programmable Switch",
+       "17": "IP Camera",
+       "18": "Video Doorbell",
+       "19": "Air Purifier",
+       "20": "Heater",
+       "21": "Air Conditioner",
+       "22": "Humidifier",
+       "23": "Dehumidifier",
+       "28": "Sprinkler",
+       "29": "Faucet",
+       "30": "Shower System",
+       "31": "Television",
+       "32": "Remote Control",
+       "33": "WiFi Router",
+       "34": "Audio Receiver",
+       "35": "TV Set Top Box",
+       "36": "TV Stick",
+};
+
+function get_arp_cache() {
+       let ret = {};
+
+       let cache = rtnl.request(rtnl.const.RTM_GETNEIGH, rtnl.const.NLM_F_DUMP, {});
+       for (let data in cache) {
+               if (!data.lladdr || !data.dst)
+                       continue;
+
+               ret[data.dst] = data.lladdr;
+       }
+
+       return ret;
+}
+
+function find_arp_entry(arp, addrs)
+{
+       for (let addr in addrs) {
+               let val = arp[addr];
+               if (val)
+                       return val;
+       }
+       return null;
+}
+
+function get_host_addr_cache()
+{
+       let arp = get_arp_cache();
+       let host_cache = ubus.call("umdns", "hosts", { array: true });
+       let host_addr = {};
+       for (let host in host_cache) {
+               let host_data = host_cache[host];
+               host_addr[host] = find_arp_entry(arp, host_data.ipv4) ??
+                                 find_arp_entry(arp, host_data.ipv6);
+       }
+
+       return host_addr;
+}
+
+function convert_txt_array(data) {
+       let txt = {};
+
+       for (let rec in data) {
+               rec = split(rec, "=", 2);
+               if (rec[1])
+                       txt[rec[0]] = rec[1];
+       }
+
+       return txt;
+}
+
+function handle_apple(txt, name)
+{
+       let ret = [];
+       let model = txt.model ?? txt.rpMd ?? txt.am;
+       if (model)
+               push(ret, `apple_model|${model}`);
+       if (name)
+               push(ret, `%device_name|mdns_implicit_device_name|${name}`);
+
+       return ret;
+}
+
+function handle_homekit(txt)
+{
+       let ret = [];
+       if (txt.ci) {
+               let type = homekit_types[txt.ci];
+               if (type)
+                       push(ret, `%class|homekit_class|${type}`);
+       }
+
+       if (txt.md)
+               push(ret, `%device|mdns_model_string|${txt.md}`);
+
+       return ret;
+}
+
+function handle_googlecast_model(txt)
+{
+       let ret = [];
+       let model = txt.model ?? txt.rpMd ?? txt.md;
+       if (model)
+               push(ret, `%device|mdns_model_string|${model}`);
+       if (txt.fn)
+               push(ret, `%device_name|mdns_device_name|${txt.fn}`);
+       if (txt.rs == 'TV')
+               push(ret, "%class|mdns_tv|TV");
+       return ret;
+}
+
+function handle_printer(txt)
+{
+       let ret = [];
+       let vendor = txt.usb_MFG;
+       let model = txt.usb_MDL ?? txt.ty ?? replace(txt.product, /^\((.*)\)$/, "$1");
+       let weight = (txt.usb_MFG && txt.usb_MDL) ? "mdns_vendor_model_string" : "mdns_model_string";
+       if (vendor)
+               push(ret, `%vendor|${weight}|${vendor}`);
+       if (model)
+               push(ret, `%device|${weight}|${model}`);
+       push(ret, "%class|mdns_printer|Printer");
+
+       return ret;
+}
+
+function handle_scanner(txt)
+{
+       let ret = [];
+       let vendor = txt.mfg;
+       let model = txt.mdl ?? txt.ty;
+       let weight = (txt.mfg && txt.mdl) ? "mdns_vendor_model_string" : "mdns_model_string";
+       if (vendor)
+               push(ret, `%vendor|${weight}|${vendor}`);
+       if (model)
+               push(ret, `%device|${weight}|${model}`);
+       push(ret, "%class|mdns_scanner|Scanner");
+
+       return ret;
+}
+
+function handle_hue(txt, name)
+{
+       let ret = [];
+       push(ret, `%vendor|mdns_service|Philips`);
+       push(ret, `%device|mdns_service|Hue`);
+       if (name)
+               push(ret, `%device_name|mdns_implicit_device_name|${name}`);
+
+       return ret;
+}
+
+function handle_fritzbox(txt)
+{
+       let ret = [];
+       push(ret, `%vendor|mdns_service|AVM`);
+       push(ret, `%device|mdns_service|FRITZ!Box`);
+
+       return ret;
+}
+
+function handle_uconfig(txt)
+{
+       let ret = [];
+       push(ret, "%class|mdns_uconfig|uconfig");
+       return ret;
+}
+
+const service_handler = {
+       "_airplay._tcp": handle_apple,
+       "_companion-link._tcp": handle_apple,
+       "_raop._tcp": (txt) => handle_apple(txt), // skip name
+       "_googlecast._tcp": handle_googlecast_model,
+       "_ipp._tcp": handle_printer,
+       "_scanner._tcp": handle_scanner,
+       "_hap._tcp": handle_homekit,
+       "_hap._udp": handle_homekit,
+       "_hue._tcp": handle_hue,
+       "_fbox._tcp": handle_fritzbox,
+       "_avmnexus._tcp": handle_fritzbox,
+       "_uconfig._udp": handle_uconfig,
+};
+
+function arp_resolve(list)
+{
+       let host_cache = ubus.call("umdns", "hosts", { array: true });
+       for (let entry in list) {
+               let iface = entry[0];
+               let host = entry[1];
+               if (!host_cache[host])
+                       continue;
+               for (let addr in host_cache[host].ipv4)
+                       rtnl.request(rtnl.const.RTM_NEWNEIGH,
+                                    rtnl.const.NLM_F_CREATE | rtnl.const.NLM_F_REPLACE,
+                                    { dev: iface, state: rtnl.const.NUD_INCOMPLETE,
+                                      flags: rtnl.const.NTF_USE, family: rtnl.const.AF_INET,
+                                      dst: addr });
+       }
+}
+
+function refresh() {
+       if (last_refresh == time())
+               return;
+
+       let host_cache = get_host_addr_cache();
+       let query_list = [];
+       let arp_pending = [];
+       let mdns_data;
+
+       for (let i = 0; i < 2; i++) {
+               mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
+               for (let service in mdns_data) {
+                       if (!service_handler[service])
+                               continue;
+                       let service_data = mdns_data[service];
+                       for (let host in service_data) {
+                               let host_entry = service_data[host].host;
+                               let iface = service_data[host].iface;
+                               if (!iface)
+                                       continue;
+                               if (!host_entry)
+                                       push(query_list, [ `${host}.${service}.local`, iface ]);
+                               else if (!host_cache[host_entry])
+                                       push(arp_pending, [ iface, host_entry ]);
+                       }
+               }
+
+               if (!length(arp_pending) && !length(query_list))
+                       break;
+
+               if (length(arp_pending) > 0)
+                       arp_resolve(arp_pending);
+
+               if (length(query_list) > 0) {
+                       for (let query in query_list)
+                               ubus.call("umdns", "query", { question: query[0], interface: query[1] });
+               }
+
+               sleep(1000);
+
+               host_cache = get_host_addr_cache();
+               mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
+       }
+
+       for (let service in mdns_data) {
+               if (!service_handler[service])
+                       continue;
+
+               let service_data = mdns_data[service];
+               for (let host in service_data) {
+                       let txt = service_data[host].txt;
+                       if (!txt)
+                               continue;
+
+                       let host_entry = service_data[host].host;
+                       if (!host_entry)
+                               continue;
+
+                       let mac = host_cache[host_entry];
+                       if (!mac)
+                               continue;
+
+                       txt = convert_txt_array(txt);
+                       let match = service_handler[service](txt, host);
+                       for (let info in match)
+                               global.device_add_data(mac, info);
+
+                       global.device_refresh(mac);
+               }
+       }
+}
+
+function init(gl) {
+       global = gl;
+       ubus = gl.ubus;
+
+       global.add_weight({
+               apple_model: 10.0,
+               mdns_device_name: 10.0,
+               mdns_implicit_device_name: 5.0,
+               mdns_vendor_model_string: 10.0,
+               mdns_service: 10.0,
+               mdns_tv: 5.0,
+               mdns_model_string: 5.0,
+               mdns_printer: 5.0,
+               mdns_scanner: 1.0,
+               homekit_class: 2.0,
+               mdns_uconfig: 10.0,
+       });
+}
+
+return { init, refresh };
diff --git a/plugins/plugin_wifi.uc b/plugins/plugin_wifi.uc
new file mode 100644 (file)
index 0000000..693d3c0
--- /dev/null
@@ -0,0 +1,320 @@
+import * as struct from "struct";
+let ubus, uloop, global, timer;
+let ap_cache = {};
+
+const ie_tags = {
+       PWR_CAPABILITY: 33,
+       HT_CAP: 45,
+       EXT_CAPAB: 127,
+       VHT_CAP: 191,
+       __EXT_START: 0x100,
+       HE_CAP: 0x100 | 35,
+       VENDOR_WPS: 0x0050f204,
+};
+
+const ie_parser_proto = {
+       reset: function() {
+               this.offset = 0;
+       },
+
+       parseAt: function(offset) {
+               let hdr = substr(this.buffer, offset, 2);
+               if (length(hdr) != 2)
+                       return null;
+
+               let data = this.hdr.unpack(hdr);
+               if (length(data != 2))
+                       return null;
+
+               let len = data[1];
+               offset += 2;
+               data[1] += 2;
+
+               if (data[0] == 221 && len >= 4) {
+                       hdr = substr(this.buffer, offset, 4);
+                       if (length(hdr) != 4)
+                               return null;
+
+                       let val = this.vendor_hdr.unpack(hdr);
+                       if (length(val) != 1 || val[0] < 0x200)
+                               return null;
+
+                       data[0] = val[0];
+                       len -= 4;
+                       offset += 4;
+               } else if (data[0] == 255 && len >= 1) {
+                       hdr = substr(this.buffer, offset, 2);
+                       if (length(hdr) != 2)
+                               return null;
+                       data[0] = 0x100 + this.hdr.unpack(hdr)[0];
+                       len -= 1;
+                       offset += 1;
+               }
+
+               data[2] = data[1];
+               data[1] = substr(this.buffer, offset, len);
+               if (length(data[1]) != len)
+                       return null;
+
+               return data;
+       },
+
+       next: function() {
+               let data = this.parseAt(this.offset);
+               if (!data)
+                       return null;
+
+               this.offset += data[2];
+               return data;
+       },
+
+       foreach: function(cb) {
+               let offset = 0;
+               let data;
+
+               while ((data = this.parseAt(offset)) != null) {
+                       offset += data[2];
+                       let ret = cb(data);
+                       if (type(ret) == "boolean" && !ret)
+                               break;
+               }
+       },
+};
+
+function ie_parser(data) {
+       let parser = {
+               offset: 0,
+               buffer: data,
+               hdr: struct.new("BB"),
+               vendor_hdr: struct.new(">I"),
+       };
+
+       proto(parser, ie_parser_proto);
+
+       return parser;
+}
+
+function format_fn(unpack_str, format)
+{
+       return (data) => {
+               data = struct.unpack(unpack_str, data);
+               if (data && data[0])
+                       data = data[0];
+               else
+                       data = 0;
+               return sprintf(format, data)
+       };
+}
+
+let unpack;
+unpack = {
+       u8: format_fn("B", "%02x"),
+       le16: format_fn("<H", "%04x"),
+       le32: format_fn("<I", "%08x"),
+       bytes: (data) => join("", map(split(data, ""), unpack.u8)),
+};
+let fingerprint_order = [
+       "htcap", "htagg", "htmcs", "vhtcap", "vhtrxmcs", "vhttxmcs",
+       "txpow", "extcap", "wps", "hemac", "hephy"
+];
+
+function format_wps_ie(data) {
+       let offset = 0;
+       let len = length(data);
+       let s = struct.new("<HH");
+
+       while (offset + 4 <= len) {
+               let hdr = s.unpack(substr(data, offset, 4));
+               let val = substr(data, offset + 4, hdr[1]);
+
+               offset += 4 + hdr[1];
+               if (hdr[0] != 0x1023)
+                       continue;
+
+               if (length(val) != hdr[1])
+                       break;
+
+               return replace(val, /[^A-Za-z0-9]/, "_");
+       }
+
+       return null;
+}
+
+function ie_fingerprint_str(id) {
+       if (id >= 0x200)
+               return sprintf("221(%08x)", id);
+       if (id >= 0x100)
+               return sprintf("255(%d)", id - 0x100);
+       return sprintf("%d", id);
+}
+
+let vendor_ie_filter = [
+       0x0050f2, // Microsoft WNN
+       0x506f9a, // WBA
+       0x8cfdf0, // Qualcom
+       0x001018, // Broadcom
+       0x000c43, // Ralink
+];
+
+function ie_fingerprint(data, mode) {
+       let caps = {
+               tags: [],
+               vendor_list: {}
+       };
+       let parser = ie_parser(data);
+
+       parser.foreach(function(ie) {
+               let skip = false;
+               let val = ie[1];
+               switch (ie[0]) {
+               case ie_tags.HT_CAP:
+                       caps.htcap = unpack.le16(substr(val, 0, 2));
+                       caps.htagg = unpack.u8(substr(val, 2, 1));
+                       caps.htmcs = unpack.le32(substr(val, 3, 4));
+                       break;
+               case ie_tags.VHT_CAP:
+                       caps.vhtcap = unpack.le32(substr(val, 0, 4));
+                       caps.vhtrxmcs = unpack.le32(substr(val, 4, 4));
+                       caps.vhttxmcs = unpack.le32(substr(val, 8, 4));
+                       break;
+               case ie_tags.EXT_CAPAB:
+                       caps.extcap = unpack.bytes(val);
+                       break;
+               case ie_tags.PWR_CAPABILITY:
+                       caps.txpow = unpack.le16(val);
+                       break;
+               case ie_tags.VENDOR_WPS:
+                       caps.wps = format_wps_ie(val);
+                       break;
+               case ie_tags.HE_CAP:
+                       if (mode != "wifi6") {
+                               skip = true;
+                               break;
+                       }
+                       caps.hemac =
+                               unpack.le16(substr(val, 4, 2)) +
+                               unpack.le32(substr(val, 0, 4));
+                       caps.hephy =
+                               unpack.le16(substr(val, 15, 2)) +
+                               unpack.le32(substr(val, 11, 4)) +
+                               unpack.le32(substr(val, 7, 4));
+                       break;
+               }
+               if (ie[0] > 0x200) {
+                       let vendor = ie[0] >> 8;
+                       if (!(vendor in vendor_ie_filter))
+                               caps.vendor_list[sprintf("%06x", vendor)] = 1;
+               }
+               if (!skip)
+                       push(caps.tags, ie[0]);
+               return null;
+       });
+
+       switch (mode) {
+       case "wifi6":
+               if (mode == "wifi6" && !caps.hemac)
+                       return null;
+               break;
+       case "wifi-vendor-oui":
+               return caps.vendor_list;
+       default:
+               break;
+       }
+
+       let tags = map(caps.tags, ie_fingerprint_str);
+       return
+               join(",", tags) + "," +
+               join(",", map(
+                       filter(fingerprint_order, (key) => !!caps[key]),
+                       (key) => `${key}:${caps[key]}`
+               ));
+}
+
+function fingerprint(mac, mode, ies) {
+       switch (mode) {
+       case "wifi4":
+               if (!ies.assoc_ie)
+                       break;
+
+               let assoc = ie_fingerprint(ies.assoc_ie, mode);
+               if (!assoc)
+                       break;
+
+               global.device_add_data(mac, `${mode}|${assoc}`);
+               break;
+       case "wifi-vendor-oui":
+               let list = ie_fingerprint(ies.assoc_ie, mode);
+               for (let oui in list) {
+                       global.device_add_data(mac, `${mode}-${oui}|1`);
+               }
+               break;
+       case "wifi6":
+       default:
+               let val = ie_fingerprint(ies.assoc_ie, mode);
+               if (!val)
+                       break;
+
+               global.device_add_data(mac, `${mode}|${val}`);
+               break;
+       }
+}
+
+const fingerprint_modes = [ "wifi4", "wifi6", "wifi-vendor-oui" ];
+
+function client_refresh(ap, mac, prev_cache)
+{
+       let ies = ubus.call(ap, "get_sta_ies", { address: mac });
+       if (type(ies) != "object" || !ies.assoc_ie)
+               return null;
+
+       ies.assoc_ie = b64dec(ies.assoc_ie);
+       if (ies.probe_ie)
+               ies.probe_ie = b64dec(ies.probe_ie);
+
+       for (let mode in fingerprint_modes)
+               fingerprint(mac, mode, ies);
+
+       return ies;
+}
+
+function refresh()
+{
+       let ap_objs = filter(ubus.list(), (name) => match(name, /^hostapd\./));
+       let prev_cache = ap_cache;
+       ap_cache = {};
+
+       timer.set(30 * 1000);
+       for (let ap in ap_objs) {
+               try {
+                       let cur_cache = {};
+                       let prev_ap_cache = prev_cache[ap] ?? {};
+
+                       ap_cache[ap] = cur_cache;
+
+                       let clients = ubus.call(ap, "get_clients").clients;
+                       for (let client in clients) {
+                               let client_cache = prev_ap_cache[client];
+                               if (!client_cache || !client_cache.assoc_ie || !client_cache.probe_ie)
+                                       client_cache = client_refresh(ap, client);
+                               global.device_refresh(client);
+                       }
+               } catch (e) {
+               }
+       }
+}
+
+function init(gl) {
+       global = gl;
+       ubus = gl.ubus;
+       uloop = gl.uloop;
+
+       global.add_weight({
+               wifi4: 2.0,
+               wifi6: 3.0,
+               "wifi-vendor-oui": 2.0
+       });
+
+       timer = uloop.timer(1000, refresh);
+}
+
+return { init, refresh };