mirror of
https://github.com/Telecominfraproject/wlan-ap.git
synced 2025-10-29 17:42:41 +00:00
607 lines
14 KiB
Ucode
607 lines
14 KiB
Ucode
'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;
|
|
};
|