From: John Crispin Date: Mon, 26 May 2025 09:12:06 +0000 (+0200) Subject: move plugins to their own folder X-Git-Url: http://git.openwrt.org/?a=commitdiff_plain;h=efc2a7c2911e7b8b5c2c45b50a8bbb0451e4469d;p=project%2Fufp.git move plugins to their own folder Signed-off-by: John Crispin --- diff --git a/files/usr/share/ufp/plugin_dhcp.uc b/files/usr/share/ufp/plugin_dhcp.uc deleted file mode 100644 index d34a582..0000000 --- a/files/usr/share/ufp/plugin_dhcp.uc +++ /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 index 308d379..0000000 --- a/files/usr/share/ufp/plugin_mdns.uc +++ /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 index 693d3c0..0000000 --- a/files/usr/share/ufp/plugin_wifi.uc +++ /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(" 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("= 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 index 0000000..d34a582 --- /dev/null +++ b/plugins/plugin_dhcp.uc @@ -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 index 0000000..308d379 --- /dev/null +++ b/plugins/plugin_mdns.uc @@ -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 index 0000000..693d3c0 --- /dev/null +++ b/plugins/plugin_wifi.uc @@ -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(" 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("= 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 };