'use strict'; import * as nl80211 from 'nl80211'; import * as libubus from 'ubus'; import { readfile, stat } from "fs"; let wifi_devices = json(readfile('/usr/share/wifi_devices.json')); let countries = json(readfile('/usr/share/iso3166.json')); let board_data = json(readfile('/etc/board.json')); export let phys = nl80211.request(nl80211.const.NL80211_CMD_GET_WIPHY, nl80211.const.NLM_F_DUMP, { split_wiphy_dump: true }); let interfaces = nl80211.request(nl80211.const.NL80211_CMD_GET_INTERFACE, nl80211.const.NLM_F_DUMP); let ubus = libubus.connect(); let wireless_status = ubus.call('network.wireless', 'status'); function find_phy(wiphy) { for (let k, phy in phys) if (phy.wiphy == wiphy) return phy; return null; } function get_noise(iface) { for (let phy in phys) { let channels = nl80211.request(nl80211.const.NL80211_CMD_GET_SURVEY, nl80211.const.NLM_F_DUMP, { dev: iface.ifname }); for (let k, channel in channels) if (channel.survey_info.frequency == iface.wiphy_freq) return channel.survey_info.noise; } return -100; } function get_country(iface) { let reg = nl80211.request(nl80211.const.NL80211_CMD_GET_REG, 0, { dev: iface.ifname }); return reg.reg_alpha2 ?? ''; } function get_max_power(iface) { let phy = find_phy(iface.wiphy); for (let k, band in phy.wiphy_bands) if (band) for (let freq in band.freqs) if (freq.freq == iface.wiphy_freq) return freq.max_tx_power;; return 0; } function get_hardware_id(iface) { let hw = { type: 'nl80211', id: 'Generic MAC80211', power_offset: 0, channel_offset: 0, }; let path = `/sys/class/ieee80211/phy${iface.wiphy}/device/`; if (stat(path) + 'vendor') { let data = []; for (let lookup in [ 'vendor', 'device', 'subsystem_vendor', 'subsystem_device' ]) push(data, trim(readfile(path + lookup), '\n')); for (let device in wifi_devices.pci) { let match = 0; for (let i = 0; i < 4; i++) if (lc(data[i]) == lc(device[i])) match++; if (match == 4) { hw.type = `${data[0]}:${data[1]} ${data[2]}:${data[3]}`; hw.power_offset = device[4]; hw.channel_offset = device[5]; hw.id = `${device[6]} ${device[7]}`; } } } let compatible = trim(readfile(`/sys/class/net/${iface.ifname}/device/of_node/compatible`), '\n'); if (compatible && wifi_devices.compatible[compatible]) { hw.id = wifi_devices.compatible[compatible][0] + ' ' + wifi_devices.compatible[compatible][1]; hw.compatible = compatible; hw.type = 'embedded'; } return hw; } const iftypes = [ 'Unknown', 'Ad-Hoc', 'Client', 'Master', 'Master (VLAN)', 'WDS', 'Monitor', 'Mesh Point', 'P2P Client', 'P2P Go', ]; export let ifaces = {}; for (let k, v in interfaces) { let iface = ifaces[v.ifname] = v; iface.mode = iftypes[iface.iftype] ?? 'unknonw', iface.noise = get_noise(iface); iface.country = get_country(iface); iface.max_power = get_max_power(iface); iface.assoclist = nl80211.request(nl80211.const.NL80211_CMD_GET_STATION, nl80211.const.NLM_F_DUMP, { dev: v.ifname }) ?? []; iface.hardware = get_hardware_id(iface); iface.bss_info = ubus.call('hostapd', 'bss_info', { iface: v.ifname }); if (!iface.bss_info) iface.bss_info = ubus.call('wpa_supplicant', 'bss_info', { iface: v.ifname }); } for (let radio, data in wireless_status) for (let k, v in data.interfaces) { if (!v.ifname || !ifaces[v.ifname]) continue; ifaces[v.ifname].ssid = v.config.ssid; ifaces[v.ifname].radio = data.config; let bss_info = ifaces[v.ifname].bss_info; let owe_transition_ifname = bss_info?.owe_transition_ifname; if (v.config.owe_transition && ifaces[owe_transition_ifname]) { ifaces[v.ifname].owe_transition_ifname = owe_transition_ifname; ifaces[owe_transition_ifname].ssid = v.config.ssid; ifaces[owe_transition_ifname].radio = data.config; ifaces[owe_transition_ifname].owe_transition_ifname = v.ifname } } function format_channel(freq) { if (freq < 1000) return 0; if (freq == 2484) return 14; if (freq == 5935) return 2; if (freq < 2484) return (freq - 2407) / 5; if (freq >= 4910 && freq <= 4980) return (freq - 4000) / 5; if (freq < 5950) return (freq - 5000) / 5; if (freq <= 45000) return (freq - 5950) / 5; if (freq >= 58320 && freq <= 70200) return (freq - 56160) / 2160; return 'unknown'; } function format_band(freq) { if (freq == 5935) return '6'; if (freq < 2484) return '2.4'; if (freq < 5950) return '5'; if (freq <= 45000) return '6'; return '60'; } function format_frequency(freq) { if (!freq) return 'unknown'; freq = '' + freq; return substr(freq, 0, 1) + '.' + substr(freq, 1); } function format_rate(rate) { if (!rate) return 'unknown'; return '' + (rate / 10) + '.' + (rate % 10); } function format_mgmt_key(key) { switch(+key) { case 1: case 11: case 12: case 13: case 14: case 15: case 16: case 17: return '802.1x'; case 2: return 'WPA PSK'; case 4: return 'FT PSK'; case 6: return 'WPA PSK2'; case 8: return 'SAE'; case 18: return 'OWE'; } return null; } function assoc_flags(data) { const assoc_mhz = { width_40: 40, width_80: 80, width_80p80: '80+80', width_160: 160, width_320: 320, width_10: 10, width_5: 5 }; let mhz = 'unknown'; for (let k, v in assoc_mhz) if (data[k]) mhz = v; const assoc_flags = { mcs: { mcs: 'MCS', }, vht_mcs: { vht_mcs: 'VHT-MCS', vht_nss: 'VHT-NSS', }, he_mcs: { he_mcs: 'HE-MCS', he_nss: 'HE-NSS', he_gi: 'HE-GI', he_dcm: 'HE-DCM', }, eht_mcs: { eht_mcs: 'EHT-MCS', eht_nss: 'EHT-NSS', eht_gi: 'EHT-GI', }, }; let flags = []; for (let k, v in assoc_flags) { if (!data[k]) continue; let first = 0; for (let name, flag in v) { if (data[name] == null) continue; push(flags, `${flag} ${data[name]}`); if (!first++) push(flags, `${mhz}MHz`); } } return flags; } function dbm2mw(dbm) { const LOG10_MAGIC = 1.25892541179; let res = 1.0; let ip = dbm / 10; let fp = dbm % 10; for (let k = 0; k < ip; k++) res *= 10; for (let k = 0; k < fp; k++) res *= 1.25892541179; return int(res); } function dbm2quality(dbm) { let quality = dbm; if (quality < -110) quality = -110; else if (quality > -40) quality = -40; quality += 110; return quality; } function hwmodelist(name) { const mode = { 'HT*': 'n', 'VHT*': 'ac', 'HE*': 'ax' }; let iface = ifaces[name]; let phy = board_data.wlan?.['phy' + iface.wiphy]; if (!phy) return ''; let htmodes = phy.info.bands[uc(iface.radio.band)].modes; let list = []; if (iface.radio.band == '2g' && 'NOHT' in htmodes) push(list, 'g/b'); for (let k, v in mode) for (let htmode in htmodes) if (wildcard(htmode, k)) push(list, v); return join('/', reverse(uniq(list))); } export function assoclist(dev) { let stations = ifaces[dev].assoclist; let ret = {}; for (let station in stations) { let sta = { mac: uc(station.mac), signal: station.sta_info.signal_avg, noise: ifaces[dev].noise, snr: station.sta_info.signal_avg - ifaces[dev].noise, inactive_time: station.sta_info.inactive_time, rx: { bitrate: format_rate(station.sta_info.rx_bitrate.bitrate), bitrate_raw: station.sta_info.rx_bitrate.bitrate, packets: station.sta_info.rx_packets, flags: assoc_flags(station.sta_info.rx_bitrate), }, tx: { bitrate: format_rate(station.sta_info.tx_bitrate.bitrate), bitrate_raw: station.sta_info.tx_bitrate.bitrate, packets: station.sta_info.tx_packets, flags: assoc_flags(station.sta_info.tx_bitrate), }, expected_throughput: station.sta_info.expected_throughput ?? 'unknown', }; ret[sta.mac] = sta; } return ret; }; export function freqlist(name) { const freq_flags = { no_10mhz: 'NO_10MHZ', no_20mhz: 'NO_20MHZ', no_ht40_minus: 'NO_HT40-', no_ht40_plus: 'NO_HT40+', no_80mhz: 'NO_80MHZ', no_160mhz: 'NO_160MHZ', indoor_only: 'INDOOR_ONLY', no_ir: 'NO_IR', no_he: 'NO_HE', }; let iface = ifaces[name]; let phy = find_phy(iface.wiphy); let channels = []; for (let k, band in phy.wiphy_bands) { if (!band) continue; let band_name = format_band(band.freqs[0].freq); for (let freq in band.freqs) { if (freq.disabled) continue; let channel = { freq: format_frequency(freq.freq), band: band_name, channel: format_channel(freq.freq), flags: [], active: iface.wiphy_freq == freq.freq, }; for (let k, v in freq_flags) if (freq[k]) push(channel.flags, v); push(channels, channel); } } return channels; }; export function info(name) { let order = []; for (let iface, data in ifaces) push(order, iface); let list = []; for (let iface in sort(order)) { if (name && iface != name) continue; let data = ifaces[iface]; let dev = { iface, ssid: data.ssid, mac: data.mac, mode: data.mode, channel: format_channel(data.wiphy_freq), freq: format_frequency(data.wiphy_freq), htmode: data?.radio?.htmode, center_freq1: format_channel(data.center_freq1) || 'unknown', center_freq2: format_channel(data.center_freq2) || 'unknown', txpower: data.wiphy_tx_power_level / 100, noise: data.noise, signal: 0, bitrate: 0, encryption: 'unknown', hwmode: hwmodelist(iface), phy: 'phy' + data.wiphy, vaps: 'no', hw_type: data.hardware.type, hw_id: data.hardware.id, power_offset: data.hardware.power_offset || 'none', channel_offset: data.hardware.channel_offset || 'none', }; let phy = find_phy(data.wiphy); for (let limit in phy.interface_combinations[0]?.limits) if (limit.types?.ap && limit.max > 1) dev.vaps = 'yes'; if (data.bss_info) { if (data.bss_info.wpa_key_mgmt && data.bss_info.wpa_pairwise) dev.encryption = `${replace(data.bss_info.wpa_key_mgmt, ' ', ' / ')} (${data.bss_info.wpa_pairwise})`; else if (data.owe_transition_ifname) dev.encryption = 'none (OWE transition)'; else dev.encryption = 'none'; } let stations = assoclist(iface); for (let k, station in stations) { dev.signal += station.signal; dev.bitrate += station.tx.bitrate_raw; } dev.signal /= length(data.assoclist) || 1; dev.bitrate /= length(data.assoclist) || 1; dev.bitrate = format_rate(dev.bitrate); dev.quality = dbm2quality(dev.signal); if (data.owe_transition_ifname) dev.owe_transition_ifname = data.owe_transition_ifname; push(list, dev); } return list; }; export function htmodelist(name) { let iface = ifaces[name]; let phy = board_data.wlan?.['phy' + iface.wiphy]; if (!phy) return []; return filter(phy.info.bands[uc(iface.radio.band)].modes, (v) => v != 'NOHT'); }; export function txpowerlist(name) { let iface = ifaces[name]; let max_power = iface.max_power / 100; let match = iface.wiphy_tx_power_level / 100; let list = []; for (let power = 0; power <= max_power; power++) { let txpower = { dbm: power, mw: dbm2mw(power), active: power == match, }; push(list, txpower); } return list; }; export function countrylist(dev) { let iface = ifaces[dev]; let list = { active: iface.country, countries, }; return list; }; export function scan(dev) { const rsn_cipher = [ 'NONE', 'WEP-40', 'TKIP', 'WRAP', 'CCMP', 'WEP-104', 'AES-OCB', 'CKIP', 'GCMP', 'GCMP-256', 'CCMP-256' ]; const ht_chan_offset = [ 'no secondary', 'above', '[reserved]', 'below' ]; const vht_chan_width = [ '20 or 40 MHz', '80 MHz', '80+80 MHz', '160 MHz' ]; const ht_chan_width = [ '20 MHz', '40 MHz or higher' ]; const SCAN_FLAG_AP = (1<<2); let params = { dev, scan_flags: SCAN_FLAG_AP, scan_ssids: [ '' ], }; let res = nl80211.request(nl80211.const.NL80211_CMD_TRIGGER_SCAN, 0, params); if (res === false) { printf("Unable to trigger scan: " + nl80211.error() + "\n"); exit(1); } res = nl80211.waitfor([ nl80211.const.NL80211_CMD_NEW_SCAN_RESULTS, nl80211.const.NL80211_CMD_SCAN_ABORTED ], 5000); if (!res) { printf("Netlink error while awaiting scan results: " + nl80211.error() + "\n"); exit(1); } else if (res.cmd == nl80211.const.NL80211_CMD_SCAN_ABORTED) { printf("Scan aborted by kernel\n"); exit(1); } let scan = nl80211.request(nl80211.const.NL80211_CMD_GET_SCAN, nl80211.const.NLM_F_DUMP, { dev }); let cells = []; for (let k, bss in scan) { bss = bss.bss; let cell = { bssid: uc(bss.bssid), frequency: format_frequency(bss.frequency), band: format_band(bss.frequency), channel: format_channel(bss.frequency), dbm: bss.signal_mbm / 100, }; if (bss.capability & (1 << 1)) cell.mode = 'Ad-Hoc'; else if (bss.capability & (1 << 0)) cell.mode = 'Master'; else cell.mode = 'Mesh Point'; cell.quality = dbm2quality(cell.dbm); for (let ie in bss.information_elements) switch(ie.type) { case 0: case 114: cell.ssid = ie.data; break; case 7: cell.country = substr(ie.data, 0, 2); break; case 48: cell.crypto = { group: rsn_cipher[+ord(ie.data, 5)] ?? '', pair: [], key_mgmt: [], }; let offset = 6; let count = +ord(ie.data, offset); offset += 2; for (let i = 0; i < count; i++) { let key = rsn_cipher[+ord(ie.data, offset + 3)]; if (key) push(cell.crypto.pair, key); offset += 4; } count = +ord(ie.data, offset); offset += 2; for (let i = 0; i < count; i++) { let key = format_mgmt_key(ord(ie.data, offset + 3)); if (key) push(cell.crypto.key_mgmt, key); offset += 4; } break; case 61: cell.ht = { primary_channel: ord(ie.data, 0), secondary_chan_off: ht_chan_offset[ord(ie.data, 1) & 0x3], chan_width: ht_chan_width[(ord(ie.data, 1) & 0x4) >> 2], }; break; case 192: cell.vht = { chan_width: vht_chan_width[ord(ie.data, 0)], center_chan_1: ord(ie.data, 1), center_chan_2: ord(ie.data, 2), }; break; }; push(cells, cell); } return cells; };