diff --git a/feeds/ucentral/ucentral-schema/Makefile b/feeds/ucentral/ucentral-schema/Makefile index d60d097ed..f2e29061c 100644 --- a/feeds/ucentral/ucentral-schema/Makefile +++ b/feeds/ucentral/ucentral-schema/Makefile @@ -4,10 +4,9 @@ PKG_NAME:=ucentral-schema PKG_RELEASE:=1 PKG_SOURCE_URL=https://github.com/Telecominfraproject/wlan-ucentral-schema.git -PKG_MIRROR_HASH:=89fd7dcbd965e3acccc39744a4e9a1dc7e41fd97facfa1638190f90604e7734b PKG_SOURCE_PROTO:=git PKG_SOURCE_DATE:=2022-05-29 -PKG_SOURCE_VERSION:=8d4384baedc0e48b1d1554d419ed217bb3aa0de5 +PKG_SOURCE_VERSION:=9877d3014b11b98eede5ea694e5566873b740ee3 PKG_MAINTAINER:=John Crispin PKG_LICENSE:=BSD-3-Clause diff --git a/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/quality-threshold.json b/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/quality-threshold.json new file mode 100644 index 000000000..8c00ce189 --- /dev/null +++ b/feeds/ucentral/ucentral-schema/files/etc/ucentral/examples/quality-threshold.json @@ -0,0 +1,89 @@ +{ + "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" + }, + "quality-thresholds" : { + "probe-request-rssi": -35, + "assoctiation-request-rssi": -35, + "client-kick-rssi": -45, + "client-kick-ban-time": 60 + } + } + ] + }, + { + "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" + } + } + } + ], + "metrics": { + "statistics": { + "interval": 120, + "types": [ "ssids", "lldp", "clients" ] + }, + "health": { + "interval": 120 + } + }, + "services": { + "lldp": { + "describe": "uCentral", + "location": "universe" + }, + "ssh": { + "port": 22 + } + } +} diff --git a/feeds/ucentral/ucrun/Makefile b/feeds/ucentral/ucrun/Makefile new file mode 100644 index 000000000..32be83b52 --- /dev/null +++ b/feeds/ucentral/ucrun/Makefile @@ -0,0 +1,34 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=ucrun +PKG_RELEASE:=1 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_URL=https://github.com/ucentral-io/ucrun.git +PKG_MIRROR_HASH:=52aeece27348611197ae5f4b96b3bdf1b5d028ae4ae284806b216d502300d07a +PKG_SOURCE_DATE:=2022-02-19 +PKG_SOURCE_VERSION:=5be6abebc4ae6057b47a5b3f0799d5ff01bc60c3 +CMAKE_INSTALL:=1 + +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=GPL + +PKG_MAINTAINER:=John Crispin + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/ucrun + SECTION:=utils + CATEGORY:=Utilities + DEPENDS:=+libubox +ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs + TITLE:=uCode main-loop daemon +endef + +define Package/ucrun/install + $(INSTALL_DIR) $(1)/usr/bin + + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/ucrun $(1)/usr/bin +endef + +$(eval $(call BuildPackage,ucrun)) diff --git a/feeds/ucentral/ucrun/patches/0001-ulog-add-ringbuffer-and-log_event-notification.patch b/feeds/ucentral/ucrun/patches/0001-ulog-add-ringbuffer-and-log_event-notification.patch new file mode 100644 index 000000000..a51ac679d --- /dev/null +++ b/feeds/ucentral/ucrun/patches/0001-ulog-add-ringbuffer-and-log_event-notification.patch @@ -0,0 +1,138 @@ +From b24a5a890ccd19b0f1b50340c79c5087f08d9447 Mon Sep 17 00:00:00 2001 +From: John Crispin +Date: Fri, 4 Mar 2022 15:56:30 +0100 +Subject: [PATCH] ulog: add ringbuffer and log_event notification + +Signed-off-by: John Crispin +--- + ucode.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 73 insertions(+), 2 deletions(-) + +diff --git a/ucode.c b/ucode.c +index cef50e2..9e0373a 100644 +--- a/ucode.c ++++ b/ucode.c +@@ -31,6 +31,17 @@ static const char *exception_types[] = { + [EXCEPTION_EXIT] = "Exit" + }; + ++struct log_buffer { ++ struct list_head list; ++ int severity; ++ char entry[]; ++}; ++ ++static LIST_HEAD(log_buffer); ++static int log_count; ++static int log_max = 100; ++static uc_value_t *log_event; ++ + static void + ucode_handle_exception(uc_vm_t *vm, uc_exception_t *ex) + { +@@ -287,6 +298,7 @@ static uc_value_t * + uc_ulog(uc_vm_t *vm, size_t nargs, int severity) + { + uc_value_t *res; ++ char *entry; + + if (!fmtfn) { + fmtfn = (uc_cfunction_t *)ucv_object_get(uc_vm_scope_get(vm), "sprintf", NULL); +@@ -300,7 +312,37 @@ uc_ulog(uc_vm_t *vm, size_t nargs, int severity) + if (!res) + return ucv_int64_new(-1); + +- ulog(severity, "%s", ucv_string_get(res)); ++ entry = ucv_string_get(res); ++ ++ if (log_max) { ++ struct log_buffer *log = calloc(1, sizeof(*log) + strlen(entry) + 1); ++ ++ strcpy(log->entry, entry); ++ log->severity = severity; ++ list_add_tail(&log->list, &log_buffer); ++ ++ if (log_event) { ++ uc_value_t *event = ucv_array_new(vm); ++ ++ ucv_array_push(event, ucv_int64_new(severity)); ++ ucv_array_push(event, ucv_string_new(entry)); ++ ++ uc_vm_stack_push(vm, ucv_get(log_event)); ++ uc_vm_stack_push(vm, ucv_get(event)); ++ uc_vm_call(vm, false, 1); ++ } ++ ++ if (log_count == log_max) { ++ struct log_buffer *first = list_first_entry(&log_buffer, struct log_buffer, list); ++ ++ list_del(&first->list); ++ free(first); ++ } else { ++ log_count++; ++ } ++ } ++ ++ ulog(severity, "%s", entry); + ucv_put(res); + + return ucv_int64_new(0); +@@ -330,11 +372,27 @@ uc_ulog_err(uc_vm_t *vm, size_t nargs) + return uc_ulog(vm, nargs, LOG_ERR); + } + ++static uc_value_t * ++uc_ulog_dump(uc_vm_t *vm, size_t nargs) ++{ ++ uc_value_t *log = ucv_array_new(vm); ++ struct log_buffer *iter; ++ ++ list_for_each_entry(iter, &log_buffer, list) { ++ uc_value_t *entry = ucv_array_new(vm); ++ ucv_array_push(entry, ucv_int64_new(iter->severity)); ++ ucv_array_push(entry, ucv_string_new(iter->entry)); ++ ucv_array_push(log, entry); ++ } ++ ++ return log; ++} ++ + static void + ucode_init_ulog(ucrun_ctx_t *ucrun) + { + uc_value_t *ulog = ucv_object_get(ucrun->scope, "ulog", NULL); +- uc_value_t *identity, *channels; ++ uc_value_t *identity, *channels, *logsize; + int flags = 0, channel; + + /* make sure the declartion is complete */ +@@ -365,6 +423,18 @@ ucode_init_ulog(ucrun_ctx_t *ucrun) + flags |= ULOG_STDIO; + } + ++ /* set the internal ring buffer size */ ++ logsize = ucv_object_get(ulog, "channels", NULL); ++ if (ucv_type(logsize) == UC_INTEGER && ucv_int64_get(logsize)) ++ log_max = ucv_int64_get(logsize); ++ ++ /* find out if ucrun wants a notification when a new log entry is generated */ ++ log_event = ucv_object_get(ulog, "event", NULL); ++ if (ucv_is_callable(log_event)) ++ ucv_get(log_event); ++ else ++ log_event = NULL; ++ + /* open the log */ + ucrun->ulog_identity = strdup(ucv_string_get(identity)); + ulog_open(flags, LOG_DAEMON, ucrun->ulog_identity); +@@ -404,6 +474,7 @@ ucode_init(ucrun_ctx_t *ucrun, int argc, const char **argv, int *rc) + uc_function_register(ucrun->scope, "ulog_note", uc_ulog_note); + uc_function_register(ucrun->scope, "ulog_warn", uc_ulog_warn); + uc_function_register(ucrun->scope, "ulog_err", uc_ulog_err); ++ uc_function_register(ucrun->scope, "ulog_dump", uc_ulog_dump); + + /* add commandline parameters */ + ARGV = ucv_array_new(&ucrun->vm); +-- +2.25.1 + diff --git a/feeds/ucentral/usteer2/Makefile b/feeds/ucentral/usteer2/Makefile new file mode 100644 index 000000000..3c0dacf46 --- /dev/null +++ b/feeds/ucentral/usteer2/Makefile @@ -0,0 +1,27 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=usteer2 +PKG_RELEASE:=1 +PKG_LICENSE:=ISC + +PKG_MAINTAINER:=John Crispin + +include $(INCLUDE_DIR)/package.mk + +define Package/usteer2 + SECTION:=utils + CATEGORY:=Utilities + DEPENDS:=+ucrun + TITLE:=wifi client steering +endef + +define Build/Compile/Default + +endef +Build/Compile = $(Build/Compile/Default) + +define Package/usteer2/install + $(CP) ./files/* $(1)/ +endef + +$(eval $(call BuildPackage,usteer2)) diff --git a/feeds/ucentral/usteer2/files/etc/config/usteer2 b/feeds/ucentral/usteer2/files/etc/config/usteer2 new file mode 100644 index 000000000..3c301d433 --- /dev/null +++ b/feeds/ucentral/usteer2/files/etc/config/usteer2 @@ -0,0 +1,9 @@ +config base + option station_update 1000 + option station_expiry 120 + +config policy + option name snr + option min_snr_kick_delay 5 + option kick_reason 5 + option interval 1000 diff --git a/feeds/ucentral/usteer2/files/etc/init.d/usteer2 b/feeds/ucentral/usteer2/files/etc/init.d/usteer2 new file mode 100755 index 000000000..93ec8eddc --- /dev/null +++ b/feeds/ucentral/usteer2/files/etc/init.d/usteer2 @@ -0,0 +1,13 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=01 + +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command /usr/bin/usteer.uc + procd_set_param respawn 3600 5 0 + procd_close_instance +} diff --git a/feeds/ucentral/usteer2/files/usr/bin/usteer.uc b/feeds/ucentral/usteer2/files/usr/bin/usteer.uc new file mode 100755 index 000000000..b82a17e93 --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/bin/usteer.uc @@ -0,0 +1,75 @@ +#!/usr/bin/ucrun + +push(REQUIRE_SEARCH_PATH, '/usr/share/usteer/*.uc'); + +global.ulog = { + identity: 'usteer', + channels: [ 'stdio', 'syslog' ], +}; + +global.ubus = { + object: 'usteer2', + + connect: function() { + printf('connected to ubus\n'); + }, + + methods: { + interfaces: { + cb: function(msg) { + return global.local.status(); + } + }, + + stations: { + cb: function(msg) { + return global.station.list(msg); + } + }, + + status: { + cb: function(msg) { + return global.station.status(); + } + }, + + command: { + cb: function(msg) { + return global.command.handle(msg); + } + }, + + get_beacon_request: { + cb: function(msg) { + let val = global.station.list(msg); + return val?.beacon_report || {}; + } + }, + + policy: { + cb: function(msg) { + return global.policy.status(msg); + }, + }, + }, +}; + +global.start = function() { + try { + global.uci = require('uci').cursor(); + global.ubus.conn = require('ubus').connect(); + + for (let module in [ 'config', 'local', 'station', 'command', 'policy' ]) { + printf('loading ' + module + '\n'); + global[module] = require(module); + if (exists(global[module], 'init')) + global[module].init(); + } + } catch(e) { + printf('exception %s\n', e); + } +}; + +global.stop = function() { + ulog_info('stopping\n'); +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/command.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/command.uc new file mode 100644 index 000000000..eed339eee --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/command.uc @@ -0,0 +1,35 @@ +function result(error, text, data) { + return { + error: error, + text: text || 'unknown', + ...(data ? { data } : {}), + }; +} + +const actions = { + // ubus call usteer2 command '{"action": "kick", "mac": "1c:57:dc:37:3c:b1", "params": {"reason": 5, "ban_time": 30}}' + kick: function(msg) { + if (global.station.kick(msg.mac, msg.params?.reason, msg.params?.ban_time)) + return result(1, 'station ' + msg.mac + ' is unknown'); + + return result(0, 'station ' + msg.mac + ' was kicked'); + }, + + // ubus call usteer2 command '{"action": "beacon_request", "mac": "1c:57:dc:37:3c:b1", "params": {"channel": 36}}' + // ubus call usteer2 get_beacon_request '{"mac": "1c:57:dc:37:3c:b1"}' + beacon_request: function(msg) { + if (!global.station.beacon_request(msg.mac, msg.params?.channel, msg.params?.op_class, msg.param?.duration)) + return result(1, 'station ' + msg.mac + ' is unknown'); + + return result(0, 'station ' + msg.mac + ' beacon-request sent'); + }, +}; + +return { + handle: function(msg) { + if (!actions[msg.action]) + return result(1, 'unknown action ' + msg.action); + + return actions[msg.action](msg); + }, +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/config.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/config.uc new file mode 100644 index 000000000..71aa4c07a --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/config.uc @@ -0,0 +1,10 @@ +return { + station_update: 1000, + station_expiry: 120, + + init: function() { + let options = uci.get_all('usteer2', '@base[-1]'); + for (let key in options) + this[key] = options[key]; + }, +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/local.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/local.uc new file mode 100644 index 000000000..24a3c5b08 --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/local.uc @@ -0,0 +1,142 @@ +let nl80211 = require("nl80211"); +let def = nl80211.const; +let subscriber; +let state = {}; +let hapd = {}; +let handlers = {}; + +function channel_survey(dev) { + /* trigger the nl80211 call that gathers channel survey data */ + let res = nl80211.request(def.NL80211_CMD_GET_SURVEY, def.NLM_F_DUMP, { dev }); + + if (!res) { + ulog_err(sprintf('failed to update survey for %s', dev)); + return; + } + + /* iterate over the result and filter out the correct channel */ + for (let survey in res) { + if (survey?.survey_info?.frequency != hapd[dev].freq) + continue; + if (survey.survey_info.noise) + hapd[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); + state[dev].time = survey.survey_info.time; + 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; + else + hapd[dev].load = load; + } + } +} + +function hapd_update() { + /* todo: prefilter frequency */ + for (let key in state) + channel_survey(key); + return 5000; +} + +function hapd_subunsub(path, sub) { + /* check if this is a hostapd instance */ + let name = split(path, '.'); + + if (length(name) != 2 || name[0] != 'hostapd') + return; + name = name[1]; + + ulog_info(sprintf('%s %s\n', sub ? 'add' : 'remove', path)); + + /* the hostapd instance disappeared */ + if (!sub) { + delete hapd[name]; + delete state[name]; + return; + } + + /* gather initial data from hostapd */ + let status = global.ubus.conn.call(path, 'get_status'); + if (!status) + return; + + let cfg = uci.get_all('usteer2', status.uci_section); + if (!cfg) + return; + + + /* subscibe to hostapd */ + subscriber.subscribe(path); + + /* tell hostapd to wait for a reply before answering probe requests */ + //global.ubus.conn.call(path, 'notify_response', { 'notify_response': 1 }); + + /* tell hostapd to enable rrm/roaming */ + global.ubus.conn.call(path, 'bss_mgmt_enable', { 'neighbor_report': 1, 'beacon_report': 1, 'bss_transition': 1 }); + + /* instantiate state */ + hapd[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; + + /* 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; + + /* trigger an initial channel survey */ + channel_survey(name); +} + +function hapd_listener(event, msg) { + hapd_subunsub(msg.path, event == 'ubus.object.add'); +} + +function hapd_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]) + if (!handler(req.type, req.data)) + reply = false; + if (!reply) + return; + req.reply(); +} + +return { + status: function() { + return hapd; + }, + + init: function() { + subscriber = global.ubus.conn.subscriber( + hapd_handle_event, + function(msg) { +// printf('2 %.J\n', 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); + + /* iterade over all existing hostapd instances and subscribe to them */ + for (let path in global.ubus.conn.list()) + hapd_subunsub(path, true); + + uloop_timeout(hapd_update, 5000); + }, + + register_handler: function(event, handler) { + /* a policy requested to be notified of action frames, register the callback */ + handlers[event] ??= []; + push(handlers[event], handler); + }, +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/policy.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/policy.uc new file mode 100644 index 000000000..5d5a267d6 --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/policy.uc @@ -0,0 +1,36 @@ +let policies = {}; +return { + init: function() { + let config = global.uci.get_all('usteer2'); + for (let section in config) { + if (config[section]['.type'] != 'policy' || !config[section].name) + continue; + let policy = require(`policy_${config[section].name}`); + if (type(policy) != 'object' || type(policy.init) != 'function') { + ulog_info('failed to load policy "%s"\n', config[section].name); + continue; + } + try { + policy.init(config[section]); + } catch(e) { + ulog_info('failed to initialze policy "%s"\n', config[section].name); + continue; + } + ulog_info('loaded policy "%s"\n', config[section].name); + policies[config[section].name] = policy; + } + }, + + status: function(msg) { + /* if no specific policies state was requested, dump the list of loaded policies */ + if (msg?.name === null) + return { policies: keys(policies) }; + + /* check if the requested policy exists and dump its state */ + if (policies[msg.name]) + return policies[msg.name].status(msg); + + /* return an empty dictionary */ + return {}; + }, +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/policy_snr.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/policy_snr.uc new file mode 100644 index 000000000..2f60249a1 --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/policy_snr.uc @@ -0,0 +1,91 @@ +let config = { + /* how many seconds must a client be below the thershold before we kick it */ + min_snr_kick_delay: 5, + /* the reson code sent when triggering the deauth (IEEE Std 802.11-2016, 9.4.1.7, Table 9-45) */ + kick_reason: 5, + /* the periodicity for checking client kick conditions */ + interval: 1000, +}; + +/* counter of how often a station was kicked */ +let kick_count = 0; + +let foo = 0; + +function snr_update() { + try { + let iface = global.local.status(); + let stations = global.station.list(); + let now = time(); + + /* iterate over all stations and kick anything that had a signal worse than the threshold for too long */ + for (let addr in stations) { + let station = stations[addr]; + if (!station.signal || !station.device) + continue; + let device = iface[station.device].config; + + if (!device?.client_kick_rssi) + continue; + + if (0) { + foo++; + if (foo > 10) + station.signal = -80; + printf(`snr check ${addr} ${station.seen} ${station.signal} ${device.client_kick_rssi}\n`); + } + printf(`${addr} ${station.signal} ${device.client_kick_rssi}\n`); + + /* ignore old stations and ones that have a good signal */ + if (now - station.seen > 2 || station.signal >= device.client_kick_rssi) { + station.snr_kick_timer = 0; + continue; + } + + /* find out how long the station had a bad signal for */ + if (!station.snr_kick_timer) + station.snr_kick_timer = now; + if (now - station.snr_kick_timer < config.min_snr_kick_delay) + continue; + + printf(`${now - station.seen}\n`); + if ((now - station.seen) > 2) + return; + + /* kick the station and ban it for the configured timeout */ + ulog_info(`kick ${addr} as signal (${station.signal}) is too low\n`); + global.station.kick(addr, config.kick_reason, device.client_kick_ban_time); + kick_count++; + } + } catch(e) { + printf(`snr exception ${e}\n`); + } + + return config.interval; +} + +function probe_handler(type, data) { + /* only send a probe request if the signal is good enough */ + return (data.signal > config.min_connect_snr) +} + +return { + init: function(data) { + /* load config and override defaults if they were set in UCI */ + for (let key in config) + if (data[key]) + config[key] = +data[key]; + + /* register a callback that will inspect probe-requests and prevent a reply if SNR is too low */ + //global.local.register_handler('probe', probe_handler); + + /* register the timer that periodically checks if a client should be kicked */ + uloop_timeout(snr_update, config.interval); + }, + + status: function(data) { + /* dump the status of this policy */ + return { config, kick_count }; + }, + +}; diff --git a/feeds/ucentral/usteer2/files/usr/share/usteer/station.uc b/feeds/ucentral/usteer2/files/usr/share/usteer/station.uc new file mode 100644 index 000000000..d8eb34893 --- /dev/null +++ b/feeds/ucentral/usteer2/files/usr/share/usteer/station.uc @@ -0,0 +1,201 @@ +let stations = {}; + +function station_add(device, addr, data, seen) { + /* only honour stations that are authenticated */ + if (!data.auth || !data.assoc) + return; + + /* if the station is new, add the initial entry */ + if (!stations[addr]) { + ulog_info(`add station ${ addr }\n`); + + /* extract the rrm bits and give them meaningful names */ + let rrm = { + link_measure: !!(data.rrm[0] & 0x1), + beacon_passive_measure: !!(data.rrm[0] & 0x10), + beacon_active_measure: !!(data.rrm[0] & 0x20), + beacon_table_measure: !!(data.rrm[0] & 0x40), + statistics_measure: !!(data.rrm[1] & 0x8), + }; + + /* add the new station */ + stations[addr] = { + rrm, + beacon_report: {}, + }; + } + + /* update device, seen and signal data */ + stations[addr].device = device; + stations[addr].seen = seen; + if (data.signal) + stations[addr].signal = data.signal; +} + +function station_del(addr) { + ulog_info(`deleting ${ addr }\n`); + delete stations[addr]; +} + +function stations_update() { + try { + /* lets not call time() multiple times */ + let seen = time(); + + /* iterate over all ssids and ask hapd for the list of associations */ + for (let device in global.local.status()) { + let clients = global.ubus.conn.call(`hostapd.${ device}`, 'get_clients'); + + for (let client in clients.clients) + if (clients.clients[client].auth) + station_add(device, client, clients.clients[client], seen); + else + station_del(client); + } + + /* purge all stations that have not been seen in a while */ + for (let station in stations) { + if (seen - stations[station].seen <= +global.config.station_expiry) + continue; + station_del(station); + } + + } catch (e) { + printf('%.J', e); + } + + /* restart the timer */ + return +global.config.station_update; +} + +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; + } + + /* store the report */ + stations[report.address].beacon_report[report.bssid] = { + seen: time(), + channel: report.channel, + rcpi: report.rcpi, + rsni: report.rsni, + }; +} + +function probe_handler(type, data) { + /* track non-associated stations SNR */ + stations[data.address] = { + signal: data.signal, + connected: false, + seen: time(), + }; + return true; +} + +function disassoc_handler(type, data) { + station_del(data.address); + + return true; +} + +return { + init: function() { + /* register the mgmt frame handlers */ + global.local.register_handler('beacon-report', beacon_report); + //global.local.register_handler('probe', probe_handler); + global.local.register_handler('disassoc', disassoc_handler); + + /* initial probe of associated stations */ + uloop_timeout(stations_update, 100); + }, + + status: function() { + let ret = { }; + let now = time(); + + /* get the list of our local APs */ + let local = global.local.status(); + + /* iterate over all APs and aggregate their associations */ + for (let device in local) { + /* iterate over all known stations */ + for (let addr, station in stations) { + /* match for the current AP */ + if (station.device != device) + continue; + + /* add the station info to the return data */ + ret[addr] = { + [device]: { + signal: station.signal, + rrm: station.rrm, + last_seen: now - station.seen, + }, + }; + + if (length(station.beacon_report)) + ret[addr][device].beacon_report = station.beacon_report; + } + } + return ret; + }, + + beacon_request: function(addr, channel, mode, op_class, duration) { + let station = stations[addr]; + + /* make sure that the station exists */ + if (!station) { + ulog_err(`beacon request on unknown station ${addr}`); + return false; + } + + /* make sure that the station supports active beacon requests */ + if (!station.rrm?.beacon_active_measure) { + ulog_err(`${addr} does not support beacon requests`); + return false; + } + + station.beacon_report = {}; + let payload = { + addr, + channel, + mode: mode || 1, + op_class: op_class || 128, + duration: duration || 100, + }; + global.ubus.conn.call(`hostapd.${station.device}`, 'rrm_beacon_req', payload); + + return true; + }, + + kick: function(addr, reason, ban_time) { + if (!exists(stations, addr)) + return -1; + + let payload = { + addr, + reason: reason || 5, + deauth: 1 + }; + + if (ban_time) + payload.ban_time = ban_time * 1000; + + /* tell hostapd to kick a station via ubus */ + global.ubus.conn.call(`hostapd.${stations[addr].device}`, 'del_client', payload); + + return 0; + }, + + list: function(msg) { + if (msg?.mac) + return stations[msg.mac] || {}; + return stations; + }, + + steer: function(addr, imminent, neighbors) { + + }, +}; diff --git a/feeds/wifi-ax/hostapd/files/hostapd.sh b/feeds/wifi-ax/hostapd/files/hostapd.sh index 3ac9e7b59..c308d1d2d 100644 --- a/feeds/wifi-ax/hostapd/files/hostapd.sh +++ b/feeds/wifi-ax/hostapd/files/hostapd.sh @@ -392,6 +392,8 @@ hostapd_common_add_bss_config() { config_add_string fils_dhcp config_add_boolean ratelimit + + config_add_string uci_section } hostapd_set_vlan_file() { @@ -627,7 +629,7 @@ hostapd_set_bss_options() { airtime_bss_weight airtime_bss_limit airtime_sta_weight \ multicast_to_unicast_all proxy_arp per_sta_vif \ eap_server eap_user_file ca_cert server_cert private_key private_key_passwd server_id \ - vendor_elements fils + vendor_elements fils uci_section set_default fils 0 set_default isolate 0 @@ -1152,6 +1154,8 @@ hostapd_set_bss_options() { append bss_conf "per_sta_vif=$per_sta_vif" "$N" fi + [ -n "$uci_section" ] && append bss_conf "uci_section=$uci_section" "$N" + json_get_values opts hostapd_bss_options for val in $opts; do append bss_conf "$val" "$N" diff --git a/feeds/wifi-ax/hostapd/patches/901-cfg-section.patch b/feeds/wifi-ax/hostapd/patches/901-cfg-section.patch new file mode 100644 index 000000000..dcfdd658f --- /dev/null +++ b/feeds/wifi-ax/hostapd/patches/901-cfg-section.patch @@ -0,0 +1,51 @@ +Index: hostapd-2021-02-20-59e9794c/hostapd/config_file.c +=================================================================== +--- hostapd-2021-02-20-59e9794c.orig/hostapd/config_file.c ++++ hostapd-2021-02-20-59e9794c/hostapd/config_file.c +@@ -2366,6 +2366,8 @@ static int hostapd_config_fill(struct ho + return 1; + } + conf->driver = driver; ++ } else if (os_strcmp(buf, "uci_section") == 0) { ++ bss->uci_section = os_strdup(pos); + } else if (os_strcmp(buf, "driver_params") == 0) { + os_free(conf->driver_params); + conf->driver_params = os_strdup(pos); +Index: hostapd-2021-02-20-59e9794c/src/ap/ap_config.h +=================================================================== +--- hostapd-2021-02-20-59e9794c.orig/src/ap/ap_config.h ++++ hostapd-2021-02-20-59e9794c/src/ap/ap_config.h +@@ -279,6 +279,7 @@ struct hostapd_bss_config { + char snoop_iface[IFNAMSIZ + 1]; + char vlan_bridge[IFNAMSIZ + 1]; + char wds_bridge[IFNAMSIZ + 1]; ++ char *uci_section; + + char *config_id; + +Index: hostapd-2021-02-20-59e9794c/src/ap/ubus.c +=================================================================== +--- hostapd-2021-02-20-59e9794c.orig/src/ap/ubus.c ++++ hostapd-2021-02-20-59e9794c/src/ap/ubus.c +@@ -467,6 +467,9 @@ hostapd_bss_get_status(struct ubus_conte + hapd->iface->cac_started ? hapd->iface->dfs_cac_ms / 1000 - now.sec : 0); + blobmsg_close_table(&b, dfs_table); + ++ if (hapd->conf->uci_section) ++ blobmsg_add_string(&b, "uci_section", hapd->conf->uci_section); ++ + ubus_send_reply(ctx, req, b.head); + + return 0; +Index: hostapd-2021-02-20-59e9794c/src/ap/ap_config.c +=================================================================== +--- hostapd-2021-02-20-59e9794c.orig/src/ap/ap_config.c ++++ hostapd-2021-02-20-59e9794c/src/ap/ap_config.c +@@ -785,6 +785,7 @@ void hostapd_config_free_bss(struct host + os_free(conf->radius_req_attr_sqlite); + os_free(conf->rsn_preauth_interfaces); + os_free(conf->ctrl_interface); ++ os_free(conf->uci_section); + os_free(conf->config_id); + os_free(conf->ca_cert); + os_free(conf->server_cert); diff --git a/patches/wifi/0019-hostapd-add-uci_section-to-status.patch b/patches/wifi/0019-hostapd-add-uci_section-to-status.patch new file mode 100644 index 000000000..02665b3d8 --- /dev/null +++ b/patches/wifi/0019-hostapd-add-uci_section-to-status.patch @@ -0,0 +1,116 @@ +From 2b9306b0b87758c9ec565f8c0dbbb55e5028b7ff Mon Sep 17 00:00:00 2001 +From: John Crispin +Date: Mon, 10 Oct 2022 11:14:57 +0200 +Subject: [PATCH] hostapd: add uci_section to status + +Signed-off-by: John Crispin +--- + .../network/services/hostapd/files/hostapd.sh | 6 ++- + .../hostapd/patches/901-cfg-section.patch | 51 +++++++++++++++++++ + package/network/services/uhttpd/Makefile | 1 + + 3 files changed, 57 insertions(+), 1 deletion(-) + create mode 100644 package/network/services/hostapd/patches/901-cfg-section.patch + +diff --git a/package/network/services/hostapd/files/hostapd.sh b/package/network/services/hostapd/files/hostapd.sh +index 3ac9e7b590..c308d1d2de 100644 +--- a/package/network/services/hostapd/files/hostapd.sh ++++ b/package/network/services/hostapd/files/hostapd.sh +@@ -392,6 +392,8 @@ hostapd_common_add_bss_config() { + config_add_string fils_dhcp + + config_add_boolean ratelimit ++ ++ config_add_string uci_section + } + + hostapd_set_vlan_file() { +@@ -627,7 +629,7 @@ hostapd_set_bss_options() { + airtime_bss_weight airtime_bss_limit airtime_sta_weight \ + multicast_to_unicast_all proxy_arp per_sta_vif \ + eap_server eap_user_file ca_cert server_cert private_key private_key_passwd server_id \ +- vendor_elements fils ++ vendor_elements fils uci_section + + set_default fils 0 + set_default isolate 0 +@@ -1152,6 +1154,8 @@ hostapd_set_bss_options() { + append bss_conf "per_sta_vif=$per_sta_vif" "$N" + fi + ++ [ -n "$uci_section" ] && append bss_conf "uci_section=$uci_section" "$N" ++ + json_get_values opts hostapd_bss_options + for val in $opts; do + append bss_conf "$val" "$N" +diff --git a/package/network/services/hostapd/patches/901-cfg-section.patch b/package/network/services/hostapd/patches/901-cfg-section.patch +new file mode 100644 +index 0000000000..dcfdd658fe +--- /dev/null ++++ b/package/network/services/hostapd/patches/901-cfg-section.patch +@@ -0,0 +1,51 @@ ++Index: hostapd-2021-02-20-59e9794c/hostapd/config_file.c ++=================================================================== ++--- hostapd-2021-02-20-59e9794c.orig/hostapd/config_file.c +++++ hostapd-2021-02-20-59e9794c/hostapd/config_file.c ++@@ -2366,6 +2366,8 @@ static int hostapd_config_fill(struct ho ++ return 1; ++ } ++ conf->driver = driver; +++ } else if (os_strcmp(buf, "uci_section") == 0) { +++ bss->uci_section = os_strdup(pos); ++ } else if (os_strcmp(buf, "driver_params") == 0) { ++ os_free(conf->driver_params); ++ conf->driver_params = os_strdup(pos); ++Index: hostapd-2021-02-20-59e9794c/src/ap/ap_config.h ++=================================================================== ++--- hostapd-2021-02-20-59e9794c.orig/src/ap/ap_config.h +++++ hostapd-2021-02-20-59e9794c/src/ap/ap_config.h ++@@ -279,6 +279,7 @@ struct hostapd_bss_config { ++ char snoop_iface[IFNAMSIZ + 1]; ++ char vlan_bridge[IFNAMSIZ + 1]; ++ char wds_bridge[IFNAMSIZ + 1]; +++ char *uci_section; ++ ++ char *config_id; ++ ++Index: hostapd-2021-02-20-59e9794c/src/ap/ubus.c ++=================================================================== ++--- hostapd-2021-02-20-59e9794c.orig/src/ap/ubus.c +++++ hostapd-2021-02-20-59e9794c/src/ap/ubus.c ++@@ -467,6 +467,9 @@ hostapd_bss_get_status(struct ubus_conte ++ hapd->iface->cac_started ? hapd->iface->dfs_cac_ms / 1000 - now.sec : 0); ++ blobmsg_close_table(&b, dfs_table); ++ +++ if (hapd->conf->uci_section) +++ blobmsg_add_string(&b, "uci_section", hapd->conf->uci_section); +++ ++ ubus_send_reply(ctx, req, b.head); ++ ++ return 0; ++Index: hostapd-2021-02-20-59e9794c/src/ap/ap_config.c ++=================================================================== ++--- hostapd-2021-02-20-59e9794c.orig/src/ap/ap_config.c +++++ hostapd-2021-02-20-59e9794c/src/ap/ap_config.c ++@@ -785,6 +785,7 @@ void hostapd_config_free_bss(struct host ++ os_free(conf->radius_req_attr_sqlite); ++ os_free(conf->rsn_preauth_interfaces); ++ os_free(conf->ctrl_interface); +++ os_free(conf->uci_section); ++ os_free(conf->config_id); ++ os_free(conf->ca_cert); ++ os_free(conf->server_cert); +diff --git a/package/network/services/uhttpd/Makefile b/package/network/services/uhttpd/Makefile +index 0ae076ca8b..a3fd2a84d9 100644 +--- a/package/network/services/uhttpd/Makefile ++++ b/package/network/services/uhttpd/Makefile +@@ -12,6 +12,7 @@ PKG_RELEASE:=1 + + PKG_SOURCE_PROTO:=git + PKG_SOURCE_URL=$(PROJECT_GIT)/project/uhttpd.git ++PKG_MIRROR_HASH:=90f737663d8495b891f0364342efb06f548a82c26ddb1595e42752f7cecdccee + PKG_SOURCE_DATE:=2022-06-01 + PKG_SOURCE_VERSION:=e3395cd90bed9b7b9fc319e79528fedcc0d947fe + PKG_MAINTAINER:=Felix Fietkau +-- +2.25.1 + diff --git a/profiles/ucentral-ap.yml b/profiles/ucentral-ap.yml index b96e1b2fd..888df82fa 100644 --- a/profiles/ucentral-ap.yml +++ b/profiles/ucentral-ap.yml @@ -43,11 +43,14 @@ packages: - ucentral-schema - ucentral-wifi - ucentral-tools + - usteer2 + - ucrun - ucode - unetd - udhcpsnoop - udnssnoop - usteer + - usteer2 - ustp - libustream-openssl - udevmand