mirror of
https://github.com/Telecominfraproject/wlan-ap.git
synced 2025-10-30 01:52:51 +00:00
usteer2: add new package
Signed-off-by: John Crispin <john@phrozen.org>
This commit is contained in:
@@ -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 <john@phrozen.org>
|
||||
PKG_LICENSE:=BSD-3-Clause
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
34
feeds/ucentral/ucrun/Makefile
Normal file
34
feeds/ucentral/ucrun/Makefile
Normal file
@@ -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 <john@phrozen.org>
|
||||
|
||||
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))
|
||||
@@ -0,0 +1,138 @@
|
||||
From b24a5a890ccd19b0f1b50340c79c5087f08d9447 Mon Sep 17 00:00:00 2001
|
||||
From: John Crispin <john@phrozen.org>
|
||||
Date: Fri, 4 Mar 2022 15:56:30 +0100
|
||||
Subject: [PATCH] ulog: add ringbuffer and log_event notification
|
||||
|
||||
Signed-off-by: John Crispin <john@phrozen.org>
|
||||
---
|
||||
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
|
||||
|
||||
27
feeds/ucentral/usteer2/Makefile
Normal file
27
feeds/ucentral/usteer2/Makefile
Normal file
@@ -0,0 +1,27 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=usteer2
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=ISC
|
||||
|
||||
PKG_MAINTAINER:=John Crispin <john@phrozen.org>
|
||||
|
||||
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))
|
||||
9
feeds/ucentral/usteer2/files/etc/config/usteer2
Normal file
9
feeds/ucentral/usteer2/files/etc/config/usteer2
Normal file
@@ -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
|
||||
13
feeds/ucentral/usteer2/files/etc/init.d/usteer2
Executable file
13
feeds/ucentral/usteer2/files/etc/init.d/usteer2
Executable file
@@ -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
|
||||
}
|
||||
75
feeds/ucentral/usteer2/files/usr/bin/usteer.uc
Executable file
75
feeds/ucentral/usteer2/files/usr/bin/usteer.uc
Executable file
@@ -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');
|
||||
};
|
||||
35
feeds/ucentral/usteer2/files/usr/share/usteer/command.uc
Normal file
35
feeds/ucentral/usteer2/files/usr/share/usteer/command.uc
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
10
feeds/ucentral/usteer2/files/usr/share/usteer/config.uc
Normal file
10
feeds/ucentral/usteer2/files/usr/share/usteer/config.uc
Normal file
@@ -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];
|
||||
},
|
||||
};
|
||||
142
feeds/ucentral/usteer2/files/usr/share/usteer/local.uc
Normal file
142
feeds/ucentral/usteer2/files/usr/share/usteer/local.uc
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
36
feeds/ucentral/usteer2/files/usr/share/usteer/policy.uc
Normal file
36
feeds/ucentral/usteer2/files/usr/share/usteer/policy.uc
Normal file
@@ -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 {};
|
||||
},
|
||||
};
|
||||
91
feeds/ucentral/usteer2/files/usr/share/usteer/policy_snr.uc
Normal file
91
feeds/ucentral/usteer2/files/usr/share/usteer/policy_snr.uc
Normal file
@@ -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 };
|
||||
},
|
||||
|
||||
};
|
||||
201
feeds/ucentral/usteer2/files/usr/share/usteer/station.uc
Normal file
201
feeds/ucentral/usteer2/files/usr/share/usteer/station.uc
Normal file
@@ -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) {
|
||||
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
51
feeds/wifi-ax/hostapd/patches/901-cfg-section.patch
Normal file
51
feeds/wifi-ax/hostapd/patches/901-cfg-section.patch
Normal file
@@ -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);
|
||||
116
patches/wifi/0019-hostapd-add-uci_section-to-status.patch
Normal file
116
patches/wifi/0019-hostapd-add-uci_section-to-status.patch
Normal file
@@ -0,0 +1,116 @@
|
||||
From 2b9306b0b87758c9ec565f8c0dbbb55e5028b7ff Mon Sep 17 00:00:00 2001
|
||||
From: John Crispin <john@phrozen.org>
|
||||
Date: Mon, 10 Oct 2022 11:14:57 +0200
|
||||
Subject: [PATCH] hostapd: add uci_section to status
|
||||
|
||||
Signed-off-by: John Crispin <john@phrozen.org>
|
||||
---
|
||||
.../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 <nbd@nbd.name>
|
||||
--
|
||||
2.25.1
|
||||
|
||||
@@ -43,11 +43,14 @@ packages:
|
||||
- ucentral-schema
|
||||
- ucentral-wifi
|
||||
- ucentral-tools
|
||||
- usteer2
|
||||
- ucrun
|
||||
- ucode
|
||||
- unetd
|
||||
- udhcpsnoop
|
||||
- udnssnoop
|
||||
- usteer
|
||||
- usteer2
|
||||
- ustp
|
||||
- libustream-openssl
|
||||
- udevmand
|
||||
|
||||
Reference in New Issue
Block a user