rrm: add background scanning

Signed-off-by: John Crispin <john@phrozen.org>
This commit is contained in:
John Crispin
2023-09-25 11:47:03 +02:00
parent d824ff4cf5
commit 9ec40d6baa
8 changed files with 429 additions and 46 deletions

View File

@@ -1,6 +1,20 @@
config base
option station_update 1000
option station_expiry 120
option beacon_request_assoc 1
option station_stats_interval 60
option scan_interval 5
option scan_dwell_time 70
list channels_2g 2412
list channels_2g 2437
list channels_2g 2462
list channels_5g 5180
list channels_5g 5260
list channels_5g 5500
list channels_5g 5580
list channels_5g 5660
list channels_5g 5745
option offset_5g 30
config policy
option name snr

View File

@@ -11,3 +11,11 @@ start_service() {
procd_set_param respawn 3600 5 0
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger rrm
}
reload_service() {
ubus call rrm reload
}

View File

@@ -3,6 +3,7 @@
push(REQUIRE_SEARCH_PATH, '/usr/share/rrmd/*.uc');
global.nl80211 = require("nl80211");
global.uloop = require("uloop");
global.fs = require('fs');
global.ulog = {
@@ -60,6 +61,20 @@ global.ubus = {
return global.policy.status(msg);
},
},
reload: {
cb: function(msg) {
global.config.init();
for (let module in [ 'local', 'station' ])
global[module].reload();
},
},
scan_dump: {
cb: function(msg) {
return global.scan.beacons;
},
},
},
};
@@ -68,7 +83,7 @@ global.start = function() {
global.uci = require('uci').cursor();
global.ubus.conn = require('ubus').connect();
for (let module in [ 'config', 'event', 'phy', 'neighbor', 'local', 'station', 'command', 'policy' ]) {
for (let module in [ 'config', 'event', 'phy', 'scan', 'neighbor', 'local', 'station', 'command', 'policy' ]) {
printf('loading ' + module + '\n');
global[module] = require(module);
if (exists(global[module], 'init'))

View File

@@ -1,9 +1,9 @@
let nl80211 = require("nl80211");
let def = nl80211.const;
let hapd_subscriber;
let interfaces_subscriber;
let ucentral_subscriber;
let state = {};
let hapd = {};
let interfaces = {};
let handlers = {};
function channel_to_freq(cur_freq, channel) {
@@ -41,10 +41,10 @@ function channel_survey(dev) {
/* iterate over the result and filter out the correct channel */
for (let survey in res) {
if (survey?.survey_info?.frequency != hapd[dev].freq)
if (survey?.survey_info?.frequency != interfaces[dev].freq)
continue;
if (survey.survey_info.noise)
hapd[dev].noise = survey.survey_info.noise;
interfaces[dev].noise = survey.survey_info.noise;
if (survey.survey_info.time && survey.survey_info.busy) {
let time = survey.survey_info.time - (state[dev].time || 0);
let busy = survey.survey_info.busy - (state[dev].busy || 0);
@@ -52,22 +52,58 @@ function channel_survey(dev) {
state[dev].busy = survey.survey_info.busy;
let load = (100 * busy) / time;
if (hapd[dev].load)
hapd[dev].load = 0.85 * hapd[dev].load + 0.15 * load;
if (interfaces[dev].load)
interfaces[dev].load = 0.85 * interfaces[dev].load + 0.15 * load;
else
hapd[dev].load = load;
interfaces[dev].load = load;
}
}
}
function hapd_update() {
function interfaces_update() {
/* todo: prefilter frequency */
for (let key in state)
channel_survey(key);
return 5000;
}
function hapd_subunsub(path, sub) {
let prev_station = {};
let stations_stats;
stations_stats = {
run: function() {
if (+global.config.station_stats_interval)
this.timer = global.uloop.timer(+global.config.station_stats_interval, stations_stats.run);
let stations = { };
let next_stations = { };
for (let iface in interfaces) {
let clients = global.ubus.conn.call('hostapd.' + iface, 'get_clients');
for (let k, v in clients?.clients) {
stations[k] = {};
next_stations[k] = {};
for (let val in [ 'bytes', 'airtime', 'packets', ])
if (v[val]) {
stations[k][val] = v[val];
next_stations[k][val] = v[val];
if (prev_station[k])
for (let type in [ 'tx', 'rx' ])
stations[k][val][type] -= prev_station[k][val][type];
}
for (let val in [ 'rate', 'mcs', 'signal', 'signal_mgmt', 'retries', 'failed', ])
stations[k][val] = v[val];
if (global.station.stations[k]) {
stations[k].bssid = global.station.stations[k].bssid;
stations[k].ssid = global.station.stations[k].ssid;
}
}
}
prev_station = next_stations;
global.event.send('rrm.stations', stations);
}
};
function interfaces_subunsub(path, sub) {
/* check if this is a hostapd instance */
let name = split(path, '.');
@@ -79,9 +115,9 @@ function hapd_subunsub(path, sub) {
/* the hostapd instance disappeared */
if (!sub) {
global.event.send('rrm.bss.del', { bssid: hapd[name] });
global.event.send('rrm.bss.del', { bssid: interfaces[name] });
global.neighbor.local_del(name);
delete hapd[name];
delete interfaces[name];
delete state[name];
return;
}
@@ -96,7 +132,7 @@ function hapd_subunsub(path, sub) {
cfg = {};
/* subscibe to hostapd */
hapd_subscriber.subscribe(path);
interfaces_subscriber.subscribe(path);
/* tell hostapd to wait for a reply before answering probe requests */
//global.ubus.conn.call(path, 'notify_response', { 'notify_response': 1 });
@@ -105,18 +141,19 @@ function hapd_subunsub(path, sub) {
global.ubus.conn.call(path, 'bss_mgmt_enable', { 'neighbor_report': 1, 'beacon_report': 1, 'bss_transition': 1 });
/* instantiate state */
hapd[name] = { };
interfaces[name] = { };
state[name] = { };
for (let prop in [ 'ssid', 'bssid', 'freq', 'channel', 'op_class', 'uci_section' ])
if (status[prop])
hapd[name][prop] = status[prop];
hapd[name].config = cfg;
interfaces[name][prop] = status[prop];
interfaces[name].config = cfg;
interfaces[name].phy = status.phy;
/* ask hostapd for the local neighbourhood report data */
let rrm = global.ubus.conn.call(path, 'rrm_nr_get_own');
if (rrm && rrm.value) {
hapd[name].rrm_nr = rrm.value;
interfaces[name].rrm_nr = rrm.value;
global.neighbor.local_add(name, rrm.value);
}
@@ -127,13 +164,16 @@ function hapd_subunsub(path, sub) {
/* send an event */
global.event.send('rrm.bss.add', {
bssid: hapd[name].bssid,
ssid: hapd[name].ssid,
freq: hapd[name].freq,
channel: hapd[name].channel,
op_class: hapd[name].op_class,
rrm_nr: hapd[name].rrm_nr,
bssid: interfaces[name].bssid,
ssid: interfaces[name].ssid,
freq: interfaces[name].freq,
channel: interfaces[name].channel,
op_class: interfaces[name].op_class,
rrm_nr: interfaces[name].rrm_nr,
});
/* tell the scanning code about the device */
global.scan.add_wdev(name, status.phy);
}
function ucentral_subunsub(sub) {
@@ -142,14 +182,14 @@ function ucentral_subunsub(sub) {
ucentral_subscriber.subscribe('ucentral');
}
function hapd_listener(event, msg) {
hapd_subunsub(msg.path, event == 'ubus.object.add');
function interfaces_listener(event, msg) {
interfaces_subunsub(msg.path, event == 'ubus.object.add');
if (msg.path == 'ucentral')
ucentral_subunsub(event == 'ubus.object.add');
}
function hapd_handle_event(req) {
function interfaces_handle_event(req) {
/* iterate over all handlers for this event type, if 1 or more handlers replied with false, do not reply to the notification */
let reply = true;
for (let handler in handlers[req.type])
@@ -169,31 +209,43 @@ function channel_switch_handler(type, data) {
}
return {
interfaces,
status: function() {
return hapd;
return interfaces;
},
reload: function() {
/* stations statistics */
if (+global.config.station_stats_interval)
stations_stat.timer = global.uloop.timer(+global.config.station_stats_interval, stations_stats.run);
else if (stations_stat.timer)
stations_stat.timer.cancel();
},
init: function() {
hapd_subscriber = global.ubus.conn.subscriber(
hapd_handle_event,
interfaces_subscriber = global.ubus.conn.subscriber(
interfaces_handle_event,
function(msg) { });
ucentral_subscriber = global.ubus.conn.subscriber(
ucentral_handle_event,
function(msg) { });
/* register a callback that will monitor hostapd instances spawning and disappearing */
global.ubus.conn.listener('ubus.object.add', hapd_listener);
global.ubus.conn.listener('ubus.object.remove', hapd_listener);
global.ubus.conn.listener('ubus.object.add', interfaces_listener);
global.ubus.conn.listener('ubus.object.remove', interfaces_listener);
/* iterade over all existing hostapd instances and subscribe to them */
for (let path in global.ubus.conn.list()) {
hapd_subunsub(path, true);
interfaces_subunsub(path, true);
if (path == 'ucentral')
ucentral_subunsub(true);
}
// uloop_timeout(hapd_update, 5000);
// uloop_timeout(interfaces_update, 5000);
global.local.register_handler('channel-switch', channel_switch_handler);
this.reload();
},
register_handler: function(event, handler) {
@@ -205,7 +257,7 @@ return {
switch_chan: function(msg) {
if (!msg.bssid || !msg.channel)
return false;
for (let bss, v in hapd) {
for (let bss, v in interfaces) {
if (v.bssid != lc(msg.bssid))
continue;
return global.ubus.conn.call('hostapd.' + bss, 'switch_chan', {
@@ -217,7 +269,7 @@ return {
},
bssid_to_phy: function(bssid) {
for (let bss, v in hapd) {
for (let bss, v in interfaces) {
if (v.bssid != lc(bssid))
continue;
let iface = global.nl80211.request(global.nl80211.const.NL80211_CMD_GET_INTERFACE, 0, { dev: bss });
@@ -227,7 +279,7 @@ return {
},
txpower: function(bssid) {
for (let bss, v in hapd) {
for (let bss, v in interfaces) {
if (v.bssid != lc(bssid))
continue;
let iface = global.nl80211.request(global.nl80211.const.NL80211_CMD_GET_INTERFACE, 0, { dev: bss });
@@ -235,4 +287,15 @@ return {
}
return 0;
},
lookup: function(phy) {
for (let bss, v in interfaces)
if (v.phy == phy)
return bss;
return null;
},
reload: function() {
},
};

View File

@@ -1,6 +1,8 @@
let uci = require("uci");
let cursor = uci ? uci.cursor() : null;
const SCAN_FLAG_AP = (1<<2);
function freq2channel(freq) {
if (freq == 2484)
return 14;
@@ -76,7 +78,7 @@ function lookup_phys() {
if (!path)
continue;
let p = { phyname, index: phy.wiphy };
let p = { path, phyname, index: phy.wiphy };
p.htmode = [];
p.band = [];
for (let band in phy.wiphy_bands) {
@@ -129,7 +131,7 @@ function lookup_phys() {
p.band = uniq(p.band);
if (!length(p.dfs_channels))
delete p.dfs_channels;
ret[path] = p;
ret[phyname] = p;
}
return ret;
}

View File

@@ -0,0 +1,156 @@
const SCAN_FLAG_AP = (1<<2);
let phys = {};
let beacons = {};
function scan(phy, params) {
if (params.wiphy_freq) {
params.center_freq1 = params.wiphy_freq + phys[phy].offset;
params.scan_ssids = [ '' ];
}
params.scan_flags = SCAN_FLAG_AP;
printf('%.J\n', params);
let res = global.nl80211.request(global.nl80211.const.NL80211_CMD_TRIGGER_SCAN, 0, params);
if (res === false)
printf("Unable to trigger scan: " + global.nl80211.error() + "\n");
else
printf('triggered scan on %s\n', params?.dev);
phys[phy].pending = true;
phys[phy].last = time();
}
function scan_parse(data) {
let seen = time();
for (let bss in data) {
bss = bss.bss;
if (!bss)
continue;
let beacon = {
freq: bss.frequency,
signal: bss.signal_mbm / 100,
seen,
};
for (let ie in bss.beacon_ies)
switch (ie.type) {
case 0:
beacon.ssid = ie.data;
break;
case 114:
beacon.meshid = ie.data;
break;
case 0xdd:
let oui = hexenc(substr(ie.data, 0, 3));
let type = ord(ie.data, 3);
let data = substr(ie.data, 4);
switch (oui) {
case '48d017':
beacon.tip = true;
switch(type) {
case 1:
if (data)
beacon.tip_name = data;
break;
case 2:
if (data)
beacon.tip_serial = data;
break;
case 3:
if (data)
beacon.tip_network_id = data;
break;
}
break;
}
break;
}
beacons[bss.bssid] = beacon;
}
printf('%.J\n', beacons);
}
function scan_timer() {
try {
for (let k, v in phys) {
if (v.pending && time() - v.last >= v.delay) {
let dev = global.local.lookup(k);
if (dev) {
let res = global.nl80211.request(global.nl80211.const.NL80211_CMD_GET_SCAN, global.nl80211.const.NLM_F_DUMP, { dev });
scan_parse(res);
}
v.pending = false;
v.last = time();
v.delay = 1;
}
if (!v.pending && time() - v.last >= global.config.scan_interval) {
let dev = global.local.lookup(k);
scan(k, {
dev,
wiphy_freq: v.channels[v.curr_chan],
measurement_duration: global.config.scan_dwell_time,
});
v.curr_chan = (v.curr_chan + 1) % v.num_chan;
}
}
} catch(e) {
printf('%.J\n', e.stacktrace[0].context);
};
return 1000;
}
return {
beacons,
add_wdev: function(dev, phy) {
if (phys[phy])
return;
let channels;
let offset = 0;
printf('%s \n', global.phy.phys[phy].band[0]);
switch (global.phy.phys[phy].band[0]) {
case '2G':
printf('fooo abc\n');
channels = global.config.channels_2g;
break;
case '5G':
channels = global.config.channels_5g;
offset = global.config.offset_5g || 0;
break;
default:
return;
}
printf('fooo, %.J\n', channels);
if (!channels)
return;
printf('fooo2\n');
let num_chan = length(channels);
printf('fooo3\n');
phys[phy] = {
channels,
offset,
num_chan,
curr_chan: 0,
delay: 5,
};
printf('%.J\n', phys[phy]);
// scan(phy, { dev });
// scan(phy, { dev, scan_ssids: [ '' ], });
},
status: function() {
return beacons;
},
init: function() {
uloop_timeout(scan_timer, 5000);
},
};

View File

@@ -29,17 +29,33 @@ function station_add(device, addr, data, seen, bssid) {
beacon_report: {},
};
if (config.beacon_request_assoc)
stations[addr].beacon_request_assoc = time();
}
/* update device, seen and signal data */
stations[addr].device = device;
stations[addr].seen = seen;
stations[addr].ssid = global.local.interfaces[device].ssid;
stations[addr].channel = global.local.interfaces[device].channel;
stations[addr].op_class = global.local.interfaces[device].op_class;
if (data.signal)
stations[addr].signal = data.signal;
/* if the station just joined, send an event */
if (add)
global.event.send('rrm.station.add', { addr, rrm: stations[addr].rrm, bssid });
/* check if a beacon_report should be triggered */
if (stations[addr].beacon_request_assoc && time() - stations[addr].beacon_request_assoc >= 30) {
global.station.beacon_request({ addr, channel: stations[addr].channel});
stations[addr].beacon_request_assoc = 0;
}
if (stations[addr].beacon_report_seen && time() - stations[addr].beacon_report_seen > 3) {
global.event.send('rrm.beacon.report', { addr, report: stations[addr].beacon_report });
stations[addr].beacon_report_seen = 0;
}
}
function station_del(addr) {
@@ -90,11 +106,15 @@ function beacon_report(type, report) {
/* make sure that the station exists */
if (!stations[report.address]) {
ulog_err(`beacon report on unknown station ${report.address}\n`);
return false;
return true;
}
if (stations[report.address].beacon_report[report.bssid])
return true;
/* store the report */
let payload = {
addr: report.address,
bssid: report.bssid,
seen: time(),
channel: report.channel,
@@ -102,7 +122,7 @@ function beacon_report(type, report) {
rsni: report.rsni,
};
stations[report.address].beacon_report[report.bssid] = payload;
global.event.send('rrm.beacon.report', payload);
stations[report.address].beacon_report_seen = time();;
}
function probe_handler(type, data) {
@@ -122,6 +142,7 @@ function disassoc_handler(type, data) {
}
return {
stations,
init: function() {
/* register the mgmt frame handlers */
global.local.register_handler('beacon-report', beacon_report);
@@ -164,7 +185,7 @@ return {
},
beacon_request: function(msg) {
if (!msg.addr || (!msg.channel && !msg.ssid))
if (!msg.addr)
return false;
let station = stations[msg.addr];
@@ -184,13 +205,12 @@ return {
station.beacon_report = {};
let payload = {
addr: msg.addr,
mode: msg.params?.mode || 1,
op_class: msg.op_class || 128,
mode: msg.mode || 1,
op_class: station.op_class || 128,
duration: msg.duration || 100,
channel: (msg.channel == null) ? station.channel : msg.channel,
};
if (msg.channel)
payload.channel = msg.channel;
else
if (msg.ssid)
payload.ssid = msg.ssid;
global.ubus.conn.call(`hostapd.${station.device}`, 'rrm_beacon_req', payload);
@@ -238,4 +258,7 @@ return {
addr: msg.addr, duration: 20, abridged: 1, neighbors }) == null;
return ret;
},
reload: function() {
},
};

View File

@@ -0,0 +1,102 @@
{
"uuid": 2,
"radios": [
{
"band": "2G",
"country": "CA",
"channel-mode": "HE",
"channel-width": 80,
"channel": 32
}
],
"interfaces": [
{
"name": "WAN",
"role": "upstream",
"services": [ "lldp" ],
"ethernet": [
{
"select-ports": [
"WAN*"
]
}
],
"ipv4": {
"addressing": "dynamic"
},
"ssids": [
{
"name": "OpenWifi",
"wifi-bands": [
"2G"
],
"bss-mode": "ap",
"encryption": {
"proto": "psk2",
"key": "OpenWifi",
"ieee80211w": "optional"
}
}
]
},
{
"name": "LAN",
"role": "downstream",
"services": [ "ssh", "lldp" ],
"ethernet": [
{
"select-ports": [
"LAN*"
]
}
],
"ipv4": {
"addressing": "static",
"subnet": "192.168.1.1/24",
"dhcp": {
"lease-first": 10,
"lease-count": 100,
"lease-time": "6h"
}
},
"ssids": [
{
"name": "OpenWifi",
"wifi-bands": [
"2G"
],
"bss-mode": "ap",
"encryption": {
"proto": "psk2",
"key": "OpenWifi",
"ieee80211w": "optional"
}
}
]
}
],
"metrics": {
"statistics": {
"interval": 120,
"types": [ "ssids", "lldp", "clients" ]
},
"health": {
"interval": 120
}
},
"services": {
"lldp": {
"describe": "uCentral",
"location": "universe"
},
"ssh": {
"port": 22
},
"rrm": {
"beacon-request-assoc": true,
"station-stats-interval": 30
}
}
}