From 9ec40d6baa9e46455d0e0d24956d04e47479067b Mon Sep 17 00:00:00 2001 From: John Crispin Date: Mon, 25 Sep 2023 11:47:03 +0200 Subject: [PATCH] rrm: add background scanning Signed-off-by: John Crispin --- feeds/ucentral/rrmd/files/etc/config/rrmd | 14 ++ feeds/ucentral/rrmd/files/etc/init.d/rrmd | 8 + feeds/ucentral/rrmd/files/usr/bin/rrmd.uc | 17 +- .../rrmd/files/usr/share/rrmd/local.uc | 133 +++++++++++---- .../ucentral/rrmd/files/usr/share/rrmd/phy.uc | 6 +- .../rrmd/files/usr/share/rrmd/scan.uc | 156 ++++++++++++++++++ .../rrmd/files/usr/share/rrmd/station.uc | 39 ++++- .../files/etc/ucentral/examples/rrm.json | 102 ++++++++++++ 8 files changed, 429 insertions(+), 46 deletions(-) create mode 100644 feeds/ucentral/rrmd/files/usr/share/rrmd/scan.uc create mode 100644 feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/rrm.json diff --git a/feeds/ucentral/rrmd/files/etc/config/rrmd b/feeds/ucentral/rrmd/files/etc/config/rrmd index 3c301d433..e988ce74f 100644 --- a/feeds/ucentral/rrmd/files/etc/config/rrmd +++ b/feeds/ucentral/rrmd/files/etc/config/rrmd @@ -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 diff --git a/feeds/ucentral/rrmd/files/etc/init.d/rrmd b/feeds/ucentral/rrmd/files/etc/init.d/rrmd index 9a7d85b69..fc54dd021 100755 --- a/feeds/ucentral/rrmd/files/etc/init.d/rrmd +++ b/feeds/ucentral/rrmd/files/etc/init.d/rrmd @@ -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 +} diff --git a/feeds/ucentral/rrmd/files/usr/bin/rrmd.uc b/feeds/ucentral/rrmd/files/usr/bin/rrmd.uc index ee4347cab..6b322c1ab 100755 --- a/feeds/ucentral/rrmd/files/usr/bin/rrmd.uc +++ b/feeds/ucentral/rrmd/files/usr/bin/rrmd.uc @@ -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')) diff --git a/feeds/ucentral/rrmd/files/usr/share/rrmd/local.uc b/feeds/ucentral/rrmd/files/usr/share/rrmd/local.uc index 710dd6c0e..57ec076e6 100644 --- a/feeds/ucentral/rrmd/files/usr/share/rrmd/local.uc +++ b/feeds/ucentral/rrmd/files/usr/share/rrmd/local.uc @@ -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() { + + }, }; diff --git a/feeds/ucentral/rrmd/files/usr/share/rrmd/phy.uc b/feeds/ucentral/rrmd/files/usr/share/rrmd/phy.uc index 2f7d14f38..e11e7670a 100644 --- a/feeds/ucentral/rrmd/files/usr/share/rrmd/phy.uc +++ b/feeds/ucentral/rrmd/files/usr/share/rrmd/phy.uc @@ -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; } diff --git a/feeds/ucentral/rrmd/files/usr/share/rrmd/scan.uc b/feeds/ucentral/rrmd/files/usr/share/rrmd/scan.uc new file mode 100644 index 000000000..0ff017dcf --- /dev/null +++ b/feeds/ucentral/rrmd/files/usr/share/rrmd/scan.uc @@ -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); + }, +}; diff --git a/feeds/ucentral/rrmd/files/usr/share/rrmd/station.uc b/feeds/ucentral/rrmd/files/usr/share/rrmd/station.uc index 38b3a04b1..23fee6245 100644 --- a/feeds/ucentral/rrmd/files/usr/share/rrmd/station.uc +++ b/feeds/ucentral/rrmd/files/usr/share/rrmd/station.uc @@ -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() { + }, }; diff --git a/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/rrm.json b/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/rrm.json new file mode 100644 index 000000000..55f3a1bec --- /dev/null +++ b/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/rrm.json @@ -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 + } + } +}