+++ /dev/null
-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 };
+++ /dev/null
-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 };
+++ /dev/null
-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 };
--- /dev/null
+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 };
--- /dev/null
+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 };
--- /dev/null
+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 };