--- /dev/null
+/Makefile
+CMakeCache.txt
+CMakeFiles
+*.cmake
+install_manifest.txt
+/usteerd
+/ap-monitor
+/fakeap
--- /dev/null
+cmake_minimum_required(VERSION 2.8)
+INCLUDE (CheckIncludeFiles)
+INCLUDE(FindPkgConfig)
+
+PROJECT(usteerd C)
+
+IF("${CMAKE_SYSTEM_NAME}" MATCHES "Linux" AND NOT NL_CFLAGS)
+ FIND_PROGRAM(PKG_CONFIG pkg-config)
+ IF(PKG_CONFIG)
+ EXECUTE_PROCESS(
+ COMMAND pkg-config --silence-errors --cflags libnl-tiny
+ OUTPUT_VARIABLE NL_CFLAGS
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ EXECUTE_PROCESS(
+ COMMAND pkg-config --silence-errors --libs libnl-tiny
+ OUTPUT_VARIABLE NL_LIBS
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ ENDIF()
+ENDIF()
+
+CHECK_INCLUDE_FILES(pcap/pcap.h HAVE_PCAP_H)
+IF(NOT HAVE_PCAP_H)
+ UNSET(HAVE_PCAP_H CACHE)
+ MESSAGE(FATAL_ERROR "pcap/pcap.h is not found")
+ENDIF()
+
+SET(SOURCES main.c local_node.c node.c sta.c policy.c ubus.c remote.c parse.c netifd.c timeout.c)
+
+IF(NL_CFLAGS)
+ ADD_DEFINITIONS(${NL_CFLAGS})
+ SET(SOURCES ${SOURCES} nl80211.c)
+ENDIF()
+
+ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations)
+
+FIND_LIBRARY(libjson NAMES json-c json)
+ADD_EXECUTABLE(usteerd ${SOURCES})
+ADD_EXECUTABLE(fakeap fakeap.c timeout.c)
+
+TARGET_LINK_LIBRARIES(usteerd ubox ubus blobmsg_json
+ ${LIBS_EXTRA} ${libjson} ${NL_LIBS})
+TARGET_LINK_LIBRARIES(fakeap ubox ubus)
+
+ADD_EXECUTABLE(ap-monitor monitor.c parse.c)
+TARGET_LINK_LIBRARIES(ap-monitor ubox pcap blobmsg_json)
+
+SET(CMAKE_INSTALL_PREFIX /usr)
+
+INSTALL(TARGETS usteerd
+ RUNTIME DESTINATION sbin
+)
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <libubox/blobmsg.h>
+#include <libubus.h>
+#include <stdio.h>
+#include <getopt.h>
+#include "utils.h"
+#include "timeout.h"
+
+static struct blob_buf b;
+static LIST_HEAD(stations);
+static struct usteer_timeout_queue tq;
+static FILE *r_fd;
+static struct ubus_object bss_obj;
+static struct ubus_context *ubus_ctx;
+static int freq = 2412;
+static int verbose;
+
+struct var {
+ int cur;
+ int min;
+ int max;
+};
+
+struct sta_data {
+ struct list_head list;
+ struct usteer_timeout probe_t;
+ struct var probe;
+ struct var signal;
+ uint8_t addr[6];
+};
+
+static void gen_val(struct var *val)
+{
+ int delta = val->max - val->min;
+ uint8_t v;
+
+ val->cur = val->min;
+ if (!delta)
+ return;
+
+ if (fread(&v, sizeof(v), 1, r_fd) != sizeof(v))
+ fprintf(stderr, "short read\n");
+ val->cur += (((unsigned int) v) * delta) / 0xff;
+}
+
+static void
+blobmsg_add_macaddr(struct blob_buf *buf, const char *name, const uint8_t *addr)
+{
+ char *s = blobmsg_alloc_string_buffer(buf, name, 20);
+ sprintf(s, MAC_ADDR_FMT, MAC_ADDR_DATA(addr));
+ blobmsg_add_string_buffer(buf);
+}
+
+static void sta_send_probe(struct sta_data *sta)
+{
+ const char *type = "probe";
+ int ret;
+ int sig = -95 + sta->signal.cur;
+
+ blob_buf_init(&b, 0);
+ blobmsg_add_macaddr(&b, "address", sta->addr);
+ blobmsg_add_u32(&b, "freq", freq);
+ blobmsg_add_u32(&b, "signal", sig);
+ ret = ubus_notify(ubus_ctx, &bss_obj, type, b.head, 100);
+ if (verbose)
+ fprintf(stderr, "STA "MAC_ADDR_FMT" probe: %d (%d ms, signal: %d)\n",
+ MAC_ADDR_DATA(sta->addr), ret, sta->probe.cur, sig);
+}
+
+static void sta_schedule_probe(struct sta_data *sta)
+{
+ gen_val(&sta->probe);
+ gen_val(&sta->signal);
+ usteer_timeout_set(&tq, &sta->probe_t, sta->probe.cur);
+}
+
+static void sta_probe(struct usteer_timeout_queue *q, struct usteer_timeout *t)
+{
+ struct sta_data *sta = container_of(t, struct sta_data, probe_t);
+
+ sta_send_probe(sta);
+ sta_schedule_probe(sta);
+}
+
+static void init_station(struct sta_data *sta)
+{
+ list_add_tail(&sta->list, &stations);
+ if (fread(&sta->addr, sizeof(sta->addr), 1, r_fd) != sizeof(sta->addr))
+ fprintf(stderr, "short read\n");
+ sta->addr[0] &= ~1;
+
+ sta_schedule_probe(sta);
+}
+
+static void create_stations(struct sta_data *ref, int n)
+{
+ struct sta_data *sta;
+ int i;
+
+ tq.cb = sta_probe;
+ sta = calloc(n, sizeof(*sta));
+ for (i = 0; i < n; i++) {
+ memcpy(sta, ref, sizeof(*sta));
+ init_station(sta);
+ sta++;
+ }
+}
+
+static int usage(const char *prog)
+{
+ fprintf(stderr, "Usage: %s <options>\n"
+ "Options:\n"
+ " -p <msec>[-<msec>]: probing interval (fixed or min-max)\n"
+ " -s <rssi>[-<rssi>]: rssi (signal strength) (fixed or min-max)\n"
+ " -n <n>: create <n> stations\n"
+ " -f <freq>: set operating frequency\n"
+ " uses parameters set before this option\n"
+ " -v: verbose\n"
+ "\n", prog);
+ return 1;
+}
+
+static bool parse_var(struct var *var, const char *str)
+{
+ char *err;
+
+ var->min = strtoul(str, &err, 0);
+ var->max = var->min;
+ if (!*err)
+ return true;
+
+ if (*err != ':')
+ return false;
+
+ var->max = strtoul(err + 1, &err, 0);
+ if (!*err)
+ return true;
+
+ return false;
+}
+
+static int
+hostapd_bss_get_clients(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ blob_buf_init(&b, 0);
+ ubus_send_reply(ctx, req, b.head);
+ return 0;
+}
+
+static const struct ubus_method bss_methods[] = {
+ UBUS_METHOD_NOARG("get_clients", hostapd_bss_get_clients),
+};
+
+static struct ubus_object_type bss_object_type =
+ UBUS_OBJECT_TYPE("hostapd_bss", bss_methods);
+
+static struct ubus_object bss_obj = {
+ .name = "hostapd.wlan0",
+ .type = &bss_object_type,
+ .methods = bss_methods,
+ .n_methods = ARRAY_SIZE(bss_methods),
+};
+
+int main(int argc, char **argv)
+{
+ struct sta_data sdata = {
+ .signal = { 0, -30, -30 },
+ .probe = { 0, 1000, 30000 },
+ };
+ int ch;
+
+ uloop_init();
+
+ r_fd = fopen("/dev/urandom", "r");
+ if (!r_fd) {
+ perror("fopen");
+ return 1;
+ }
+
+ usteer_timeout_init(&tq);
+
+ while ((ch = getopt(argc, argv, "p:s:f:n:v")) != -1) {
+ switch(ch) {
+ case 'p':
+ if (!parse_var(&sdata.probe, optarg))
+ goto usage;
+ break;
+ case 's':
+ if (!parse_var(&sdata.signal, optarg))
+ goto usage;
+ break;
+ case 'f':
+ freq = atoi(optarg);
+ break;
+ case 'n':
+ create_stations(&sdata, atoi(optarg));
+ break;
+ case 'v':
+ verbose++;
+ break;
+ default:
+ goto usage;
+ }
+ }
+
+ ubus_ctx = ubus_connect(NULL);
+ if (!ubus_ctx) {
+ fprintf(stderr, "Failed to connect to ubus\n");
+ return 1;
+ }
+
+ ubus_add_uloop(ubus_ctx);
+
+ if (ubus_add_object(ubus_ctx, &bss_obj)) {
+ fprintf(stderr, "Failed to register AP ubus object\n");
+ return 1;
+ }
+ uloop_run();
+
+ uloop_done();
+ return 0;
+usage:
+ return usage(argv[0]);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/ethernet.h>
+#ifdef linux
+#include <netinet/ether.h>
+#endif
+#include <net/if.h>
+#include <stdlib.h>
+
+#include <libubox/avl-cmp.h>
+#include <libubox/blobmsg_json.h>
+#include "usteer.h"
+#include "node.h"
+
+AVL_TREE(local_nodes, avl_strcmp, false, NULL);
+static struct blob_buf b;
+static char *node_up_script;
+
+static void
+usteer_local_node_state_reset(struct usteer_local_node *ln)
+{
+ if (ln->req_state == REQ_IDLE)
+ return;
+
+ ubus_abort_request(ubus_ctx, &ln->req);
+ ln->req_state = REQ_IDLE;
+}
+
+static void
+usteer_free_node(struct ubus_context *ctx, struct usteer_local_node *ln)
+{
+ struct usteer_node_handler *h;
+
+ list_for_each_entry(h, &node_handlers, list) {
+ if (!h->free_node)
+ continue;
+ h->free_node(&ln->node);
+ }
+
+ usteer_local_node_state_reset(ln);
+ usteer_sta_node_cleanup(&ln->node);
+ uloop_timeout_cancel(&ln->req_timer);
+ uloop_timeout_cancel(&ln->update);
+ avl_delete(&local_nodes, &ln->node.avl);
+ ubus_unregister_subscriber(ctx, &ln->ev);
+ free(ln);
+}
+
+static void
+usteer_handle_remove(struct ubus_context *ctx, struct ubus_subscriber *s,
+ uint32_t id)
+{
+ struct usteer_local_node *ln = container_of(s, struct usteer_local_node, ev);
+
+ usteer_free_node(ctx, ln);
+}
+
+static int
+usteer_handle_event(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ enum {
+ EVENT_ADDR,
+ EVENT_SIGNAL,
+ EVENT_TARGET,
+ EVENT_FREQ,
+ __EVENT_MAX
+ };
+ struct blobmsg_policy policy[__EVENT_MAX] = {
+ [EVENT_ADDR] = { .name = "address", .type = BLOBMSG_TYPE_STRING },
+ [EVENT_SIGNAL] = { .name = "signal", .type = BLOBMSG_TYPE_INT32 },
+ [EVENT_TARGET] = { .name = "target", .type = BLOBMSG_TYPE_STRING },
+ [EVENT_FREQ] = { .name = "freq", .type = BLOBMSG_TYPE_INT32 },
+ };
+ enum usteer_event_type ev_type = __EVENT_TYPE_MAX;
+ struct blob_attr *tb[__EVENT_MAX];
+ struct usteer_local_node *ln;
+ struct usteer_node *node;
+ int signal = NO_SIGNAL;
+ int freq = 0;
+ const char *addr_str;
+ const uint8_t *addr;
+ int i;
+ bool ret;
+
+ usteer_update_time();
+
+ for (i = 0; i < ARRAY_SIZE(event_types); i++) {
+ if (strcmp(method, event_types[i]) != 0)
+ continue;
+
+ ev_type = i;
+ break;
+ }
+
+ ln = container_of(obj, struct usteer_local_node, ev.obj);
+ node = &ln->node;
+ blobmsg_parse(policy, __EVENT_MAX, tb, blob_data(msg), blob_len(msg));
+ if (!tb[EVENT_ADDR] || !tb[EVENT_FREQ])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (tb[EVENT_SIGNAL])
+ signal = (int32_t) blobmsg_get_u32(tb[EVENT_SIGNAL]);
+
+ if (tb[EVENT_FREQ])
+ freq = blobmsg_get_u32(tb[EVENT_FREQ]);
+
+ addr_str = blobmsg_data(tb[EVENT_ADDR]);
+ addr = (uint8_t *) ether_aton(addr_str);
+ if (!addr)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ ret = usteer_handle_sta_event(node, addr, ev_type, freq, signal);
+
+ MSG(DEBUG, "received %s event from %s, signal=%d, freq=%d, handled:%s\n",
+ method, addr_str, signal, freq, ret ? "true" : "false");
+
+ return ret ? 0 : 17 /* WLAN_STATUS_AP_UNABLE_TO_HANDLE_NEW_STA */;
+}
+
+static void
+usteer_local_node_assoc_update(struct sta_info *si, struct blob_attr *data)
+{
+ enum {
+ MSG_ASSOC,
+ __MSG_MAX,
+ };
+ static struct blobmsg_policy policy[__MSG_MAX] = {
+ [MSG_ASSOC] = { "assoc", BLOBMSG_TYPE_BOOL },
+ };
+ struct blob_attr *tb[__MSG_MAX];
+
+ blobmsg_parse(policy, __MSG_MAX, tb, blobmsg_data(data), blobmsg_data_len(data));
+ if (tb[MSG_ASSOC] && blobmsg_get_u8(tb[MSG_ASSOC]))
+ si->connected = 1;
+
+ if (si->node->freq < 4000)
+ si->sta->seen_2ghz = 1;
+ else
+ si->sta->seen_5ghz = 1;
+}
+
+static void
+usteer_local_node_set_assoc(struct usteer_local_node *ln, struct blob_attr *cl)
+{
+ struct usteer_node *node = &ln->node;
+ struct usteer_node_handler *h;
+ struct blob_attr *cur;
+ struct sta_info *si;
+ struct sta *sta;
+ int n_assoc = 0;
+ int rem;
+
+ list_for_each_entry(si, &node->sta_info, node_list) {
+ if (si->connected)
+ si->connected = 2;
+ }
+
+ blobmsg_for_each_attr(cur, cl, rem) {
+ uint8_t *addr = (uint8_t *) ether_aton(blobmsg_name(cur));
+ bool create;
+
+ if (!addr)
+ continue;
+
+ sta = usteer_sta_get(addr, true);
+ si = usteer_sta_info_get(sta, node, &create);
+ list_for_each_entry(h, &node_handlers, list) {
+ if (!h->update_sta)
+ continue;
+
+ h->update_sta(node, si);
+ }
+ usteer_local_node_assoc_update(si, cur);
+ if (si->connected == 1)
+ n_assoc++;
+ }
+
+ node->n_assoc = n_assoc;
+
+ list_for_each_entry(si, &node->sta_info, node_list) {
+ if (si->connected != 2)
+ continue;
+
+ si->connected = 0;
+ usteer_sta_info_update_timeout(si, config.local_sta_timeout);
+ MSG(VERBOSE, "station "MAC_ADDR_FMT" disconnected from node %s\n",
+ MAC_ADDR_DATA(si->sta->addr), usteer_node_name(node));
+ }
+}
+
+static void
+usteer_local_node_list_cb(struct ubus_request *req, int type, struct blob_attr *msg)
+{
+ enum {
+ MSG_FREQ,
+ MSG_CLIENTS,
+ __MSG_MAX,
+ };
+ static struct blobmsg_policy policy[__MSG_MAX] = {
+ [MSG_FREQ] = { "freq", BLOBMSG_TYPE_INT32 },
+ [MSG_CLIENTS] = { "clients", BLOBMSG_TYPE_TABLE },
+ };
+ struct blob_attr *tb[__MSG_MAX];
+ struct usteer_local_node *ln;
+ struct usteer_node *node;
+
+ ln = container_of(req, struct usteer_local_node, req);
+ node = &ln->node;
+
+ blobmsg_parse(policy, __MSG_MAX, tb, blob_data(msg), blob_len(msg));
+ if (!tb[MSG_FREQ] || !tb[MSG_CLIENTS])
+ return;
+
+ node->freq = blobmsg_get_u32(tb[MSG_FREQ]);
+ usteer_local_node_set_assoc(ln, tb[MSG_CLIENTS]);
+}
+
+static void
+usteer_local_node_rrm_nr_cb(struct ubus_request *req, int type, struct blob_attr *msg)
+{
+ static const struct blobmsg_policy policy = {
+ "value", BLOBMSG_TYPE_ARRAY
+ };
+ struct usteer_local_node *ln;
+ struct blob_attr *tb;
+
+ ln = container_of(req, struct usteer_local_node, req);
+
+ blobmsg_parse(&policy, 1, &tb, blob_data(msg), blob_len(msg));
+ if (!tb)
+ return;
+
+ usteer_node_set_blob(&ln->node.rrm_nr, tb);
+}
+
+static void
+usteer_local_node_req_cb(struct ubus_request *req, int ret)
+{
+ struct usteer_local_node *ln;
+
+ ln = container_of(req, struct usteer_local_node, req);
+ uloop_timeout_set(&ln->req_timer, 1);
+}
+
+static void
+usteer_add_rrm_data(struct usteer_local_node *ln, struct usteer_node *node)
+{
+ if (node == &ln->node)
+ return;
+
+ if (!node->rrm_nr)
+ return;
+
+ if (strcmp(ln->node.ssid, node->ssid) != 0)
+ return;
+
+ blobmsg_add_field(&b, BLOBMSG_TYPE_ARRAY, "",
+ blobmsg_data(node->rrm_nr),
+ blobmsg_data_len(node->rrm_nr));
+}
+
+static void
+usteer_local_node_prepare_rrm_set(struct usteer_local_node *ln)
+{
+ struct usteer_remote_node *rn;
+ struct usteer_node *node;
+ void *c;
+
+ c = blobmsg_open_array(&b, "list");
+ avl_for_each_element(&local_nodes, node, avl)
+ usteer_add_rrm_data(ln, node);
+ avl_for_each_element(&remote_nodes, rn, avl)
+ usteer_add_rrm_data(ln, &rn->node);
+ blobmsg_close_array(&b, c);
+}
+
+static void
+usteer_local_node_state_next(struct uloop_timeout *timeout)
+{
+ struct usteer_local_node *ln;
+
+ ln = container_of(timeout, struct usteer_local_node, req_timer);
+
+ ln->req_state++;
+ if (ln->req_state >= __REQ_MAX) {
+ ln->req_state = REQ_IDLE;
+ return;
+ }
+
+ blob_buf_init(&b, 0);
+ switch (ln->req_state) {
+ case REQ_CLIENTS:
+ ubus_invoke_async(ubus_ctx, ln->obj_id, "get_clients", b.head, &ln->req);
+ ln->req.data_cb = usteer_local_node_list_cb;
+ break;
+ case REQ_RRM_SET_LIST:
+ usteer_local_node_prepare_rrm_set(ln);
+ ubus_invoke_async(ubus_ctx, ln->obj_id, "rrm_nr_set", b.head, &ln->req);
+ ln->req.data_cb = NULL;
+ break;
+ case REQ_RRM_GET_OWN:
+ ubus_invoke_async(ubus_ctx, ln->obj_id, "rrm_nr_get_own", b.head, &ln->req);
+ ln->req.data_cb = usteer_local_node_rrm_nr_cb;
+ break;
+ default:
+ break;
+ }
+ ln->req.complete_cb = usteer_local_node_req_cb;
+ ubus_complete_request_async(ubus_ctx, &ln->req);
+}
+
+static void
+usteer_local_node_update(struct uloop_timeout *timeout)
+{
+ struct usteer_local_node *ln;
+ struct usteer_node_handler *h;
+ struct usteer_node *node;
+
+ ln = container_of(timeout, struct usteer_local_node, update);
+ node = &ln->node;
+
+ MSG_T("local_sta_udpate", "timeout (%u) expired\n",
+ config.local_sta_update);
+
+ list_for_each_entry(h, &node_handlers, list) {
+ if (!h->update_node)
+ continue;
+
+ h->update_node(node);
+ }
+
+ usteer_local_node_state_reset(ln);
+ uloop_timeout_set(&ln->req_timer, 1);
+ usteer_local_node_kick(ln);
+ uloop_timeout_set(timeout, config.local_sta_update);
+}
+
+static struct usteer_local_node *
+usteer_get_node(struct ubus_context *ctx, const char *name)
+{
+ struct usteer_local_node *ln;
+ struct usteer_node *node;
+ char *str;
+
+ ln = avl_find_element(&local_nodes, name, ln, node.avl);
+ if (ln)
+ return ln;
+
+ ln = calloc_a(sizeof(*ln), &str, strlen(name) + 1);
+ node = &ln->node;
+ node->type = NODE_TYPE_LOCAL;
+ node->avl.key = strcpy(str, name);
+ ln->ev.remove_cb = usteer_handle_remove;
+ ln->ev.cb = usteer_handle_event;
+ ln->update.cb = usteer_local_node_update;
+ ln->req_timer.cb = usteer_local_node_state_next;
+ ubus_register_subscriber(ctx, &ln->ev);
+ avl_insert(&local_nodes, &node->avl);
+ uloop_timeout_set(&ln->update, 1);
+ INIT_LIST_HEAD(&node->sta_info);
+
+ return ln;
+}
+
+static void
+usteer_node_run_update_script(struct usteer_node *node)
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ char *val;
+
+ if (!node_up_script)
+ return;
+
+ val = alloca(strlen(node_up_script) + strlen(ln->iface) + 8);
+ sprintf(val, "%s '%s'", node_up_script, ln->iface);
+ if (system(val))
+ fprintf(stderr, "failed to execute %s\n", val);
+}
+
+static void
+usteer_register_node(struct ubus_context *ctx, const char *name, uint32_t id)
+{
+ struct usteer_local_node *ln;
+ struct usteer_node_handler *h;
+ const char *iface;
+ int offset = sizeof("hostapd.") - 1;
+
+ iface = name + offset;
+ if (strncmp(name, "hostapd.", iface - name) != 0)
+ return;
+
+ MSG(INFO, "Connecting to local node %s\n", name);
+ ln = usteer_get_node(ctx, name);
+ ln->obj_id = id;
+ ln->iface = usteer_node_name(&ln->node) + offset;
+ ln->ifindex = if_nametoindex(iface);
+
+ blob_buf_init(&b, 0);
+ blobmsg_add_u32(&b, "notify_response", 1);
+ ubus_invoke(ctx, id, "notify_response", b.head, NULL, NULL, 1000);
+
+ blob_buf_init(&b, 0);
+ blobmsg_add_u8(&b, "neighbor_report", 1);
+ blobmsg_add_u8(&b, "beacon_report", 1);
+ blobmsg_add_u8(&b, "bss_transition", 1);
+ ubus_invoke(ctx, id, "bss_mgmt_enable", b.head, NULL, NULL, 1000);
+
+ ubus_subscribe(ctx, &ln->ev, id);
+
+ list_for_each_entry(h, &node_handlers, list) {
+ if (!h->init_node)
+ continue;
+
+ h->init_node(&ln->node);
+ }
+
+ usteer_node_run_update_script(&ln->node);
+}
+
+static void
+usteer_event_handler(struct ubus_context *ctx, struct ubus_event_handler *ev,
+ const char *type, struct blob_attr *msg)
+{
+ static const struct blobmsg_policy policy[2] = {
+ { .name = "id", .type = BLOBMSG_TYPE_INT32 },
+ { .name = "path", .type = BLOBMSG_TYPE_STRING },
+ };
+ struct blob_attr *tb[2];
+ const char *path;
+
+ blobmsg_parse(policy, 2, tb, blob_data(msg), blob_len(msg));
+
+ if (!tb[0] || !tb[1])
+ return;
+
+ path = blobmsg_data(tb[1]);
+ usteer_register_node(ctx, path, blobmsg_get_u32(tb[0]));
+}
+
+static void
+usteer_register_events(struct ubus_context *ctx)
+{
+ static struct ubus_event_handler handler = {
+ .cb = usteer_event_handler
+ };
+
+ ubus_register_event_handler(ctx, &handler, "ubus.object.add");
+}
+
+static void
+node_list_cb(struct ubus_context *ctx, struct ubus_object_data *obj, void *priv)
+{
+ usteer_register_node(ctx, obj->path, obj->id);
+}
+
+void config_set_node_up_script(struct blob_attr *data)
+{
+ const char *val = blobmsg_get_string(data);
+ struct usteer_node *node;
+
+ if (node_up_script && !strcmp(val, node_up_script))
+ return;
+
+ free(node_up_script);
+
+ if (!strlen(val)) {
+ node_up_script = NULL;
+ return;
+ }
+
+ node_up_script = strdup(val);
+
+ avl_for_each_element(&local_nodes, node, avl)
+ usteer_node_run_update_script(node);
+}
+
+void config_get_node_up_script(struct blob_buf *buf)
+{
+ if (!node_up_script)
+ return;
+
+ blobmsg_add_string(buf, "node_up_script", node_up_script);
+}
+
+void
+usteer_local_nodes_init(struct ubus_context *ctx)
+{
+ usteer_register_events(ctx);
+ ubus_lookup(ctx, "hostapd.*", node_list_cb, NULL);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <unistd.h>
+#include <stdarg.h>
+#include <syslog.h>
+
+#include "usteer.h"
+
+struct ubus_context *ubus_ctx;
+struct usteer_config config = {};
+uint64_t current_time;
+
+LIST_HEAD(node_handlers);
+
+const char * const event_types[__EVENT_TYPE_MAX] = {
+ [EVENT_TYPE_PROBE] = "probe",
+ [EVENT_TYPE_AUTH] = "auth",
+ [EVENT_TYPE_ASSOC] = "assoc",
+};
+
+void debug_msg(int level, const char *func, int line, const char *format, ...)
+{
+ va_list ap;
+
+ if (config.debug_level < level)
+ return;
+
+ if (!config.syslog)
+ fprintf(stderr, "[%s:%d] ", func, line);
+
+ va_start(ap, format);
+ if (config.syslog)
+ vsyslog(level >= MSG_DEBUG ? LOG_DEBUG : LOG_INFO, format, ap);
+ else
+ vfprintf(stderr, format, ap);
+ va_end(ap);
+
+}
+
+void debug_msg_cont(int level, const char *format, ...)
+{
+ va_list ap;
+
+ if (config.debug_level < level)
+ return;
+
+ va_start(ap, format);
+ vfprintf(stderr, format, ap);
+ va_end(ap);
+}
+
+void usteer_init_defaults(void)
+{
+ memset(&config, 0, sizeof(config));
+
+ config.sta_block_timeout = 30 * 1000;
+ config.local_sta_timeout = 120 * 1000;
+ config.local_sta_update = 1 * 1000;
+ config.max_retry_band = 5;
+ config.seen_policy_timeout = 30 * 1000;
+ config.band_steering_threshold = 5;
+ config.load_balancing_threshold = 5;
+ config.remote_update_interval = 1000;
+ config.initial_connect_delay = 0;
+ config.remote_node_timeout = 120 * 1000;
+
+ config.roam_kick_delay = 100;
+ config.roam_scan_tries = 3;
+ config.roam_scan_interval = 10 * 1000;
+ config.roam_trigger_interval = 60 * 1000;
+
+ config.load_kick_enabled = false;
+ config.load_kick_threshold = 75;
+ config.load_kick_delay = 10 * 1000;
+ config.load_kick_min_clients = 10;
+ config.load_kick_reason_code = 5; /* WLAN_REASON_DISASSOC_AP_BUSY */
+
+ config.debug_level = MSG_FATAL;
+}
+
+void usteer_update_time(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ current_time = (uint64_t) ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+static int usage(const char *prog)
+{
+ fprintf(stderr, "Usage: %s [options]\n"
+ "Options:\n"
+ " -v: Increase debug level (repeat for more messages):\n"
+ " 1: info messages\n"
+ " 2: debug messages\n"
+ " 3: verbose debug messages\n"
+ " 4: include network messages\n"
+ " 5: include extra testing messages\n"
+ " -i <name>: Connect to other instances on interface <name>\n"
+ " -s: Output log messages via syslog instead of stderr\n"
+ "\n", prog);
+ return 1;
+}
+
+int main(int argc, char **argv)
+{
+ int ch;
+
+ usteer_init_defaults();
+
+ while ((ch = getopt(argc, argv, "i:sv")) != -1) {
+ switch(ch) {
+ case 'v':
+ config.debug_level++;
+ break;
+ case 's':
+ config.syslog = true;
+ break;
+ case 'i':
+ usteer_interface_add(optarg);
+ break;
+ default:
+ return usage(argv[0]);
+ }
+ }
+
+ openlog("usteer", 0, LOG_USER);
+
+ usteer_update_time();
+ uloop_init();
+
+ ubus_ctx = ubus_connect(NULL);
+ if (!ubus_ctx) {
+ fprintf(stderr, "Failed to connect to ubus\n");
+ return -1;
+ }
+
+ ubus_add_uloop(ubus_ctx);
+ usteer_ubus_init(ubus_ctx);
+ usteer_interface_init();
+ usteer_local_nodes_init(ubus_ctx);
+ uloop_run();
+
+ uloop_done();
+ return 0;
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <netinet/udp.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+
+#include <pcap/pcap.h>
+
+#include <libubox/blobmsg_json.h>
+
+#include "usteer.h"
+#include "remote.h"
+
+static pcap_t *pcap;
+static int pkt_offset;
+
+/* IP header */
+struct ip_header {
+ uint8_t ip_vhl; /* version << 4 | header length >> 2 */
+ uint8_t ip_tos; /* type of service */
+ uint16_t ip_len; /* total length */
+ uint16_t ip_id; /* identification */
+ uint16_t ip_off; /* fragment offset field */
+#define IP_RF 0x8000 /* reserved fragment flag */
+#define IP_DF 0x4000 /* dont fragment flag */
+#define IP_MF 0x2000 /* more fragments flag */
+#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
+ uint8_t ip_ttl; /* time to live */
+ uint8_t ip_p; /* protocol */
+ uint16_t ip_sum; /* checksum */
+ struct in_addr ip_src, ip_dst; /* source and dest address */
+};
+#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
+#define IP_V(ip) (((ip)->ip_vhl) >> 4)
+
+struct udp_header {
+ uint16_t uh_sport; /* source port */
+ uint16_t uh_dport; /* destination port */
+ uint16_t uh_ulen; /* udp length */
+ uint16_t uh_sum; /* udp checksum */
+};
+
+
+static void
+decode_sta(struct blob_attr *data)
+{
+ struct apmsg_sta msg;
+
+ if (!parse_apmsg_sta(&msg, data))
+ return;
+
+ fprintf(stderr, "\t\tSta "MAC_ADDR_FMT" signal=%d connected=%d timeout=%d\n",
+ MAC_ADDR_DATA(msg.addr), msg.signal, msg.connected, msg.timeout);
+}
+
+static void
+decode_node(struct blob_attr *data)
+{
+ struct apmsg_node msg;
+ struct blob_attr *cur;
+ int rem;
+
+ if (!parse_apmsg_node(&msg, data))
+ return;
+
+ fprintf(stderr, "\tNode %s, freq=%d, n_assoc=%d, noise=%d load=%d max_assoc=%d\n",
+ msg.name, msg.freq, msg.n_assoc, msg.noise, msg.load, msg.max_assoc);
+ if (msg.rrm_nr) {
+ fprintf(stderr, "\t\tRRM:");
+ blobmsg_for_each_attr(cur, msg.rrm_nr, rem) {
+ if (!blobmsg_check_attr(cur, false))
+ continue;
+ if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
+ continue;
+ fprintf(stderr, " %s", blobmsg_get_string(cur));
+ }
+ fprintf(stderr, "\n");
+ }
+
+ if (msg.script_data) {
+ char *data = blobmsg_format_json(msg.script_data, true);
+ fprintf(stderr, "\t\tScript data: %s\n", data);
+ free(data);
+ }
+
+ blob_for_each_attr(cur, msg.stations, rem)
+ decode_sta(cur);
+}
+
+static void
+decode_packet(struct blob_attr *data)
+{
+ struct apmsg msg;
+ struct blob_attr *cur;
+ int rem;
+
+ if (!parse_apmsg(&msg, data)) {
+ fprintf(stderr, "missing fields\n");
+ return;
+ }
+
+ fprintf(stderr, "id=%08x, seq=%d\n", msg.id, msg.seq);
+
+ blob_for_each_attr(cur, msg.nodes, rem)
+ decode_node(cur);
+}
+
+static void
+recv_packet(unsigned char *user, const struct pcap_pkthdr *hdr,
+ const unsigned char *packet)
+{
+ char addr[INET_ADDRSTRLEN];
+ struct ip_header *ip;
+ struct udp_header *uh;
+ struct blob_attr *data;
+ int len = hdr->caplen;
+ int hdrlen;
+
+ len -= pkt_offset;
+ packet += pkt_offset;
+ ip = (void *) packet;
+
+ hdrlen = IP_HL(ip) * 4;
+ if (hdrlen < 20 || hdrlen >= len)
+ return;
+
+ len -= hdrlen;
+ packet += hdrlen;
+
+ inet_ntop(AF_INET, &ip->ip_src, addr, sizeof(addr));
+
+ hdrlen = sizeof(*uh);
+ if (len <= hdrlen)
+ return;
+
+ uh = (void *) packet;
+ packet += hdrlen;
+ len -= hdrlen;
+
+ if (uh->uh_dport != htons(APMGR_PORT))
+ return;
+
+ data = (void *) packet;
+
+ fprintf(stderr, "[%s]: len=%d ", addr, len);
+
+ if (len != blob_pad_len(data)) {
+ fprintf(stderr, "invalid data\n");
+ return;
+ }
+
+ decode_packet(data);
+}
+
+int main(int argc, char **argv)
+{
+ static char errbuf[PCAP_ERRBUF_SIZE];
+ struct bpf_program fp;
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
+ return 1;
+ }
+
+ pcap = pcap_open_live(argv[1], APMGR_BUFLEN, 1, 1000, errbuf);
+ if (!pcap) {
+ fprintf(stderr, "Failed to open interface %s: %s\n", argv[1], errbuf);
+ return 1;
+ }
+
+ pcap_compile(pcap, &fp, "port "APMGR_PORT_STR, 1, PCAP_NETMASK_UNKNOWN);
+ pcap_setfilter(pcap, &fp);
+
+ switch (pcap_datalink(pcap)) {
+ case DLT_EN10MB:
+ pkt_offset = 14;
+ break;
+ case DLT_RAW:
+ pkt_offset = 0;
+ break;
+ default:
+ fprintf(stderr, "Invalid link type\n");
+ return -1;
+ }
+
+ pcap_loop(pcap, 0, recv_packet, NULL);
+ pcap_close(pcap);
+
+ return 0;
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include "usteer.h"
+#include "node.h"
+
+static struct blob_buf b;
+
+static void
+netifd_parse_interface_config(struct usteer_local_node *ln, struct blob_attr *msg)
+{
+ static const struct blobmsg_policy policy = {
+ .name = "maxassoc",
+ .type = BLOBMSG_TYPE_INT32,
+ };
+ struct blob_attr *cur;
+ int val = 0;
+
+ blobmsg_parse(&policy, 1, &cur, blobmsg_data(msg), blobmsg_data_len(msg));
+ if (cur)
+ val = blobmsg_get_u32(cur);
+
+ ln->node.max_assoc = val;
+ ln->netifd.status_complete = true;
+}
+
+static void
+netifd_parse_interface(struct usteer_local_node *ln, struct blob_attr *msg)
+{
+ enum {
+ N_IF_CONFIG,
+ N_IF_NAME,
+ __N_IF_MAX
+ };
+ static const struct blobmsg_policy policy[__N_IF_MAX] = {
+ [N_IF_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_TABLE },
+ [N_IF_NAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING },
+ };
+ struct blob_attr *tb[__N_IF_MAX];
+
+ if (blobmsg_type(msg) != BLOBMSG_TYPE_TABLE)
+ return;
+
+ blobmsg_parse(policy, __N_IF_MAX, tb, blobmsg_data(msg), blobmsg_data_len(msg));
+ if (!tb[N_IF_CONFIG] || !tb[N_IF_NAME])
+ return;
+
+ if (strcmp(blobmsg_get_string(tb[N_IF_NAME]), ln->iface) != 0)
+ return;
+
+ netifd_parse_interface_config(ln, tb[N_IF_CONFIG]);
+}
+
+static void
+netifd_parse_radio(struct usteer_local_node *ln, struct blob_attr *msg)
+{
+ static const struct blobmsg_policy policy = {
+ .name = "interfaces",
+ .type = BLOBMSG_TYPE_ARRAY,
+ };
+ struct blob_attr *cur, *iface;
+ int rem;
+
+ if (blobmsg_type(msg) != BLOBMSG_TYPE_TABLE)
+ return;
+
+ blobmsg_parse(&policy, 1, &iface, blobmsg_data(msg), blobmsg_data_len(msg));
+ if (!iface)
+ return;
+
+ blobmsg_for_each_attr(cur, iface, rem)
+ netifd_parse_interface(ln, cur);
+}
+
+static void
+netifd_status_cb(struct ubus_request *req, int type, struct blob_attr *msg)
+{
+ struct usteer_local_node *ln;
+ struct blob_attr *cur;
+ int rem;
+
+ ln = container_of(req, struct usteer_local_node, netifd.req);
+ ln->netifd.req_pending = false;
+
+ blobmsg_for_each_attr(cur, msg, rem)
+ netifd_parse_radio(ln, cur);
+}
+
+static void netifd_update_node(struct usteer_node *node)
+{
+ struct usteer_local_node *ln;
+ uint32_t id;
+
+ ln = container_of(node, struct usteer_local_node, node);
+ if (ln->netifd.status_complete)
+ return;
+
+ if (ln->netifd.req_pending)
+ ubus_abort_request(ubus_ctx, &ln->netifd.req);
+
+ if (ubus_lookup_id(ubus_ctx, "network.wireless", &id))
+ return;
+
+ blob_buf_init(&b, 0);
+ ubus_invoke_async(ubus_ctx, id, "status", b.head, &ln->netifd.req);
+ ln->netifd.req.data_cb = netifd_status_cb;
+ ubus_complete_request_async(ubus_ctx, &ln->netifd.req);
+ ln->netifd.req_pending = true;
+}
+
+static void netifd_init_node(struct usteer_node *node)
+{
+ struct usteer_local_node *ln;
+
+ ln = container_of(node, struct usteer_local_node, node);
+ ln->netifd.status_complete = false;
+ netifd_update_node(node);
+}
+
+static void netifd_free_node(struct usteer_node *node)
+{
+ struct usteer_local_node *ln;
+
+ ln = container_of(node, struct usteer_local_node, node);
+ if (ln->netifd.req_pending)
+ ubus_abort_request(ubus_ctx, &ln->netifd.req);
+}
+
+static struct usteer_node_handler netifd_handler = {
+ .init_node = netifd_init_node,
+ .update_node = netifd_update_node,
+ .free_node = netifd_free_node,
+};
+
+static void __usteer_init usteer_netifd_init(void)
+{
+ list_add(&netifd_handler.list, &node_handlers);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#define _GNU_SOURCE
+#include <linux/if_ether.h>
+#include <net/if.h>
+
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <linux/nl80211.h>
+#include <unl.h>
+
+#include "usteer.h"
+#include "node.h"
+
+static struct unl unl;
+static struct nlattr *tb[NL80211_ATTR_MAX + 1];
+
+struct nl80211_survey_req {
+ void (*cb)(void *priv, struct usteer_survey_data *d);
+ void *priv;
+};
+
+struct nl80211_scan_req {
+ void (*cb)(void *priv, struct usteer_scan_result *r);
+ void *priv;
+};
+
+struct nl80211_freqlist_req {
+ void (*cb)(void *priv, struct usteer_freq_data *f);
+ void *priv;
+};
+
+static int nl80211_survey_result(struct nl_msg *msg, void *arg)
+{
+ static struct nla_policy survey_policy[NL80211_SURVEY_INFO_MAX + 1] = {
+ [NL80211_SURVEY_INFO_FREQUENCY] = { .type = NLA_U32 },
+ [NL80211_SURVEY_INFO_NOISE] = { .type = NLA_U8 },
+ [NL80211_SURVEY_INFO_CHANNEL_TIME] = { .type = NLA_U64 },
+ [NL80211_SURVEY_INFO_CHANNEL_TIME_BUSY] = { .type = NLA_U64 },
+ };
+ struct nlattr *tb[NL80211_ATTR_MAX + 1];
+ struct nlattr *tb_s[NL80211_SURVEY_INFO_MAX + 1];
+ struct nl80211_survey_req *req = arg;
+ struct usteer_survey_data data = {};
+ struct genlmsghdr *gnlh;
+
+ gnlh = nlmsg_data(nlmsg_hdr(msg));
+ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+ genlmsg_attrlen(gnlh, 0), NULL);
+
+ if (!tb[NL80211_ATTR_SURVEY_INFO])
+ return NL_SKIP;
+
+ if (nla_parse_nested(tb_s, NL80211_SURVEY_INFO_MAX,
+ tb[NL80211_ATTR_SURVEY_INFO], survey_policy))
+ return NL_SKIP;
+
+ if (!tb_s[NL80211_SURVEY_INFO_FREQUENCY])
+ return NL_SKIP;
+
+ data.freq = nla_get_u32(tb_s[NL80211_SURVEY_INFO_FREQUENCY]);
+
+ if (tb_s[NL80211_SURVEY_INFO_NOISE])
+ data.noise = (int8_t) nla_get_u8(tb_s[NL80211_SURVEY_INFO_NOISE]);
+
+ if (tb_s[NL80211_SURVEY_INFO_CHANNEL_TIME] &&
+ tb_s[NL80211_SURVEY_INFO_CHANNEL_TIME_BUSY]) {
+ data.time = nla_get_u64(tb_s[NL80211_SURVEY_INFO_CHANNEL_TIME]);
+ data.time_busy = nla_get_u64(tb_s[NL80211_SURVEY_INFO_CHANNEL_TIME_BUSY]);
+ }
+
+ req->cb(req->priv, &data);
+
+ return NL_SKIP;
+}
+
+static void nl80211_get_survey(struct usteer_node *node, void *priv,
+ void (*cb)(void *priv, struct usteer_survey_data *d))
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ struct nl80211_survey_req req = {
+ .priv = priv,
+ .cb = cb,
+ };
+ struct nl_msg *msg;
+
+ if (!ln->nl80211.present)
+ return;
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_GET_SURVEY, true);
+ NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ln->ifindex);
+ unl_genl_request(&unl, msg, nl80211_survey_result, &req);
+
+nla_put_failure:
+ return;
+}
+
+static void nl80211_update_node_result(void *priv, struct usteer_survey_data *d)
+{
+ struct usteer_local_node *ln = priv;
+ uint32_t delta = 0, delta_busy = 0;
+
+ if (d->freq != ln->node.freq)
+ return;
+
+ if (d->noise)
+ ln->node.noise = d->noise;
+
+ if (ln->time) {
+ delta = d->time - ln->time;
+ delta_busy = d->time_busy - ln->time_busy;
+ }
+
+ ln->time = d->time;
+ ln->time_busy = d->time_busy;
+
+ if (delta) {
+ float cur = (100 * delta_busy) / delta;
+
+ if (ln->load_ewma < 0)
+ ln->load_ewma = cur;
+ else
+ ln->load_ewma = 0.85 * ln->load_ewma + 0.15 * cur;
+
+ ln->node.load = ln->load_ewma;
+ }
+}
+
+static void nl80211_update_node(struct uloop_timeout *t)
+{
+ struct usteer_local_node *ln = container_of(t, struct usteer_local_node, nl80211.update);
+
+ uloop_timeout_set(t, 1000);
+ ln->ifindex = if_nametoindex(ln->iface);
+ nl80211_get_survey(&ln->node, ln, nl80211_update_node_result);
+}
+
+static void nl80211_init_node(struct usteer_node *node)
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ struct genlmsghdr *gnlh;
+ static bool _init = false;
+ struct nl_msg *msg;
+
+ if (node->type != NODE_TYPE_LOCAL)
+ return;
+
+ ln->nl80211.present = false;
+ ln->wiphy = -1;
+
+ if (!ln->ifindex) {
+ MSG(INFO, "No ifindex found for node %s\n", usteer_node_name(node));
+ return;
+ }
+
+ if (!_init) {
+ if (unl_genl_init(&unl, "nl80211") < 0) {
+ unl_free(&unl);
+ MSG(INFO, "nl80211 init failed\n");
+ return;
+ }
+
+ _init = true;
+ }
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_GET_INTERFACE, false);
+ NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ln->ifindex);
+ unl_genl_request_single(&unl, msg, &msg);
+ if (!msg)
+ return;
+
+ gnlh = nlmsg_data(nlmsg_hdr(msg));
+ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+ genlmsg_attrlen(gnlh, 0), NULL);
+
+ if (!tb[NL80211_ATTR_WIPHY])
+ goto nla_put_failure;
+
+ ln->wiphy = nla_get_u32(tb[NL80211_ATTR_WIPHY]);
+
+ if (tb[NL80211_ATTR_SSID]) {
+ int len = nla_len(tb[NL80211_ATTR_SSID]);
+
+ if (len >= sizeof(node->ssid))
+ len = sizeof(node->ssid) - 1;
+
+ memcpy(node->ssid, nla_data(tb[NL80211_ATTR_SSID]), len);
+ node->ssid[len] = 0;
+ }
+
+ MSG(INFO, "Found nl80211 phy on wdev %s, ssid=%s\n", usteer_node_name(node), node->ssid);
+ ln->load_ewma = -1;
+ ln->nl80211.present = true;
+ ln->nl80211.update.cb = nl80211_update_node;
+ nl80211_update_node(&ln->nl80211.update);
+
+nla_put_failure:
+ nlmsg_free(msg);
+ return;
+}
+
+static void nl80211_free_node(struct usteer_node *node)
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+
+ if (!ln->nl80211.present)
+ return;
+
+ uloop_timeout_cancel(&ln->nl80211.update);
+}
+
+static void nl80211_update_sta(struct usteer_node *node, struct sta_info *si)
+{
+ struct nlattr *tb_sta[NL80211_STA_INFO_MAX + 1];
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ struct genlmsghdr *gnlh;
+ struct nl_msg *msg;
+ int signal = NO_SIGNAL;
+
+ if (!ln->nl80211.present)
+ return;
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_GET_STATION, false);
+ NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ln->ifindex);
+ NLA_PUT(msg, NL80211_ATTR_MAC, ETH_ALEN, si->sta->addr);
+ unl_genl_request_single(&unl, msg, &msg);
+ if (!msg)
+ return;
+
+ gnlh = nlmsg_data(nlmsg_hdr(msg));
+ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+ genlmsg_attrlen(gnlh, 0), NULL);
+
+ if (!tb[NL80211_ATTR_STA_INFO])
+ goto nla_put_failure;
+
+ if (nla_parse_nested(tb_sta, NL80211_STA_INFO_MAX,
+ tb[NL80211_ATTR_STA_INFO], NULL))
+ goto nla_put_failure;
+
+ if (tb_sta[NL80211_STA_INFO_SIGNAL_AVG])
+ signal = (int8_t) nla_get_u8(tb_sta[NL80211_STA_INFO_SIGNAL_AVG]);
+
+ usteer_sta_info_update(si, signal, true);
+
+nla_put_failure:
+ nlmsg_free(msg);
+ return;
+}
+
+static int nl80211_scan_result(struct nl_msg *msg, void *arg)
+{
+ static struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = {
+ [NL80211_BSS_FREQUENCY] = { .type = NLA_U32 },
+ [NL80211_BSS_CAPABILITY] = { .type = NLA_U16 },
+ [NL80211_BSS_SIGNAL_MBM] = { .type = NLA_U32 },
+ };
+ struct nlattr *tb[NL80211_ATTR_MAX + 1];
+ struct nlattr *bss[NL80211_BSS_MAX + 1];
+ struct nl80211_scan_req *req = arg;
+ struct usteer_scan_result data = {
+ .signal = -127,
+ };
+ struct genlmsghdr *gnlh;
+ struct nlattr *ie_attr;
+ int ielen = 0;
+ uint8_t *ie;
+
+ gnlh = nlmsg_data(nlmsg_hdr(msg));
+ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+ genlmsg_attrlen(gnlh, 0), NULL);
+
+ if (!tb[NL80211_ATTR_BSS])
+ return NL_SKIP;
+
+ if (nla_parse_nested(bss, NL80211_BSS_MAX, tb[NL80211_ATTR_BSS],
+ bss_policy))
+ return NL_SKIP;
+
+ if (!bss[NL80211_BSS_BSSID] ||
+ !bss[NL80211_BSS_FREQUENCY])
+ return NL_SKIP;
+
+ data.freq = nla_get_u32(bss[NL80211_BSS_FREQUENCY]);
+ memcpy(data.bssid, nla_data(bss[NL80211_BSS_BSSID]), sizeof(data.bssid));
+
+ if (bss[NL80211_BSS_SIGNAL_MBM]) {
+ int32_t signal = nla_get_u32(bss[NL80211_BSS_SIGNAL_MBM]);
+ data.signal = signal / 100;
+ }
+
+ ie_attr = bss[NL80211_BSS_INFORMATION_ELEMENTS];
+ if (!ie_attr)
+ ie_attr = bss[NL80211_BSS_BEACON_IES];
+
+ if (!ie_attr)
+ goto skip_ie;
+
+ ie = (uint8_t *) nla_data(ie_attr);
+ ielen = nla_len(ie_attr);
+ for (; ielen >= 2 && ielen >= ie[1];
+ ielen -= ie[1] + 2, ie += ie[1] + 2) {
+ if (ie[0] == 0) { /* SSID */
+ if (ie[1] > 32)
+ continue;
+
+ memcpy(data.ssid, ie + 2, ie[1]);
+ }
+ }
+
+skip_ie:
+ req->cb(req->priv, &data);
+
+ return NL_SKIP;
+}
+
+static int nl80211_scan_event_cb(struct nl_msg *msg, void *data)
+{
+ struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+
+ switch (gnlh->cmd) {
+ case NL80211_CMD_NEW_SCAN_RESULTS:
+ case NL80211_CMD_SCAN_ABORTED:
+ unl_loop_done(&unl);
+ break;
+ }
+
+ return NL_SKIP;
+}
+
+static int nl80211_scan(struct usteer_node *node, struct usteer_scan_request *req,
+ void *priv, void (*cb)(void *priv, struct usteer_scan_result *r))
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ struct nl80211_scan_req reqdata = {
+ .priv = priv,
+ .cb = cb,
+ };
+ struct nl_msg *msg;
+ struct nlattr *cur;
+ int i, ret;
+
+ if (!ln->nl80211.present)
+ return -ENODEV;
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_TRIGGER_SCAN, false);
+ NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ln->ifindex);
+
+ if (!req->passive) {
+ cur = nla_nest_start(msg, NL80211_ATTR_SCAN_SSIDS);
+ NLA_PUT(msg, 1, 0, "");
+ nla_nest_end(msg, cur);
+ }
+
+ NLA_PUT_U32(msg, NL80211_ATTR_SCAN_FLAGS, NL80211_SCAN_FLAG_AP);
+
+ if (req->n_freq) {
+ cur = nla_nest_start(msg, NL80211_ATTR_SCAN_FREQUENCIES);
+ for (i = 0; i < req->n_freq; i++)
+ NLA_PUT_U32(msg, i, req->freq[i]);
+ nla_nest_end(msg, cur);
+ }
+
+ unl_genl_subscribe(&unl, "scan");
+ ret = unl_genl_request(&unl, msg, NULL, NULL);
+ if (ret < 0)
+ goto done;
+
+ unl_genl_loop(&unl, nl80211_scan_event_cb, NULL);
+
+done:
+ unl_genl_unsubscribe(&unl, "scan");
+ if (ret < 0)
+ return ret;
+
+ if (!cb)
+ return 0;
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_GET_SCAN, true);
+ NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ln->ifindex);
+ unl_genl_request(&unl, msg, nl80211_scan_result, &reqdata);
+
+ return 0;
+
+nla_put_failure:
+ nlmsg_free(msg);
+ return -ENOMEM;
+}
+
+static int nl80211_wiphy_result(struct nl_msg *msg, void *arg)
+{
+ struct nl80211_freqlist_req *req = arg;
+ struct nlattr *tb[NL80211_ATTR_MAX + 1];
+ struct nlattr *tb_band[NL80211_BAND_ATTR_MAX + 1];
+ struct nlattr *tb_freq[NL80211_FREQUENCY_ATTR_MAX + 1];
+ struct nlattr *nl_band;
+ struct nlattr *nl_freq;
+ struct nlattr *cur;
+ struct genlmsghdr *gnlh;
+ int rem_band;
+ int rem_freq;
+
+ gnlh = nlmsg_data(nlmsg_hdr(msg));
+ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+ genlmsg_attrlen(gnlh, 0), NULL);
+
+ if (!tb[NL80211_ATTR_WIPHY_BANDS])
+ return NL_SKIP;
+
+ nla_for_each_nested(nl_band, tb[NL80211_ATTR_WIPHY_BANDS], rem_band) {
+ nla_parse(tb_band, NL80211_BAND_ATTR_MAX, nla_data(nl_band),
+ nla_len(nl_band), NULL);
+
+ if (!tb_band[NL80211_BAND_ATTR_FREQS])
+ continue;
+
+ nla_for_each_nested(nl_freq, tb_band[NL80211_BAND_ATTR_FREQS],
+ rem_freq) {
+ struct usteer_freq_data f = {};
+
+ nla_parse(tb_freq, NL80211_FREQUENCY_ATTR_MAX,
+ nla_data(nl_freq), nla_len(nl_freq), NULL);
+
+ if (tb_freq[NL80211_FREQUENCY_ATTR_DISABLED])
+ continue;
+
+ if (tb_freq[NL80211_FREQUENCY_ATTR_NO_IR])
+ continue;
+
+ cur = tb_freq[NL80211_FREQUENCY_ATTR_FREQ];
+ if (!cur)
+ continue;
+
+ f.freq = nla_get_u32(cur);
+ f.dfs = !!tb_freq[NL80211_FREQUENCY_ATTR_RADAR];
+
+ cur = tb_freq[NL80211_FREQUENCY_ATTR_MAX_TX_POWER];
+ if (cur)
+ f.txpower = nla_get_u32(cur) / 100;
+
+ req->cb(req->priv, &f);
+ }
+ }
+
+ return NL_SKIP;
+}
+
+static void nl80211_get_freqlist(struct usteer_node *node, void *priv,
+ void (*cb)(void *priv, struct usteer_freq_data *f))
+{
+ struct usteer_local_node *ln = container_of(node, struct usteer_local_node, node);
+ struct nl80211_freqlist_req req = {
+ .priv = priv,
+ .cb = cb
+ };
+ struct nl_msg *msg;
+
+ if (!ln->nl80211.present)
+ return;
+
+ msg = unl_genl_msg(&unl, NL80211_CMD_GET_WIPHY, false);
+
+ NLA_PUT_U32(msg, NL80211_ATTR_WIPHY, ln->wiphy);
+ NLA_PUT_FLAG(msg, NL80211_ATTR_SPLIT_WIPHY_DUMP);
+
+ unl_genl_request(&unl, msg, nl80211_wiphy_result, &req);
+
+ return;
+
+nla_put_failure:
+ nlmsg_free(msg);
+}
+
+static struct usteer_node_handler nl80211_handler = {
+ .init_node = nl80211_init_node,
+ .free_node = nl80211_free_node,
+ .update_sta = nl80211_update_sta,
+ .get_survey = nl80211_get_survey,
+ .get_freqlist = nl80211_get_freqlist,
+ .scan = nl80211_scan,
+};
+
+static void __usteer_init usteer_nl80211_init(void)
+{
+ list_add(&nl80211_handler.list, &node_handlers);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include "usteer.h"
+
+void usteer_node_set_blob(struct blob_attr **dest, struct blob_attr *val)
+{
+ int new_len;
+ int len;
+
+ if (!val) {
+ free(*dest);
+ *dest = NULL;
+ return;
+ }
+
+ len = *dest ? blob_pad_len(*dest) : 0;
+ new_len = blob_pad_len(val);
+ if (new_len != len)
+ *dest = realloc(*dest, new_len);
+ memcpy(*dest, val, new_len);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#ifndef __APMGR_NODE_H
+#define __APMGR_NODE_H
+
+#include "usteer.h"
+
+enum local_req_state {
+ REQ_IDLE,
+ REQ_CLIENTS,
+ REQ_RRM_SET_LIST,
+ REQ_RRM_GET_OWN,
+ __REQ_MAX
+};
+
+struct usteer_local_node {
+ struct usteer_node node;
+
+ struct ubus_subscriber ev;
+ struct uloop_timeout update;
+
+ const char *iface;
+ int ifindex;
+ int wiphy;
+
+ struct ubus_request req;
+ struct uloop_timeout req_timer;
+ int req_state;
+
+ uint32_t obj_id;
+
+ float load_ewma;
+ int load_thr_count;
+
+ uint64_t time, time_busy;
+
+ struct {
+ bool present;
+ struct uloop_timeout update;
+ } nl80211;
+ struct {
+ struct ubus_request req;
+ bool req_pending;
+ bool status_complete;
+ } netifd;
+};
+
+struct interface;
+struct usteer_remote_node {
+ struct avl_node avl;
+ const char *name;
+
+ struct usteer_node node;
+ struct interface *iface;
+
+ int check;
+};
+
+extern struct avl_tree local_nodes;
+extern struct avl_tree remote_nodes;
+
+#endif
--- /dev/null
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=usteer
+PKG_VERSION:=$(shell git show -s --format=%cd --date=short)
+PKG_RELEASE:=1
+
+PKG_BUILD_PARALLEL:=1
+
+PKG_FILE_DEPENDS:=$(CURDIR)/../..
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+define Build/Prepare
+ mkdir -p $(PKG_BUILD_DIR)
+ ln -s $(CURDIR)/../../.git $(PKG_BUILD_DIR)/.git
+ cd $(PKG_BUILD_DIR) && git checkout .
+endef
+
+define Package/usteer
+ SECTION:=net
+ CATEGORY:=Network
+ DEPENDS:=+libubox +libubus +libblobmsg-json +libnl-tiny
+ TITLE:=OpenWrt AP roaming assist daemon
+endef
+
+define Package/usteer/conffiles
+/etc/config/usteer
+endef
+
+define Package/usteer/install
+ $(INSTALL_DIR) $(1)/sbin $(1)/etc/init.d $(1)/etc/config
+ $(CP) ./files/* $(1)/
+ $(CP) $(PKG_BUILD_DIR)/usteer $(1)/sbin/
+endef
+
+$(eval $(call BuildPackage,usteer))
--- /dev/null
+config usteer
+ option 'network' 'lan'
+ option 'syslog' '1'
+ option 'debug_level' '2'
--- /dev/null
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2013 OpenWrt.org
+
+START=50
+USE_PROCD=1
+
+NAME=usteer
+PROG=/sbin/usteer
+
+. /lib/functions/network.sh
+. /usr/share/libubox/jshn.sh
+. /lib/functions.sh
+
+load_ifaces() {
+ local network="$(uci get usteer.@usteer[-1].network)"
+ for n in $network; do
+ local device
+ json_load "$(ifstatus $n)"
+ json_get_var device l3_device
+ echo -n "$device "
+ done
+}
+
+uci_option_to_json_bool() {
+ local cfg="$1"
+ local option="$2"
+ local val
+
+ config_get_bool val "$cfg" $option
+ [ -n "$val" ] && json_add_boolean $option $val
+}
+
+uci_option_to_json_string() {
+ local cfg="$1"
+ local option="$2"
+ local val
+
+ config_get val "$cfg" "$option"
+ [ -n "$val" ] && json_add_string $option "$val"
+}
+
+uci_option_to_json() {
+ local cfg="$1"
+ local option="$2"
+ local val
+
+ config_get val "$cfg" $option
+ [ -n "$val" ] && json_add_int $option $val
+}
+
+uci_usteer() {
+ local cfg="$1"
+
+ uci_option_to_json_bool "$cfg" syslog
+ uci_option_to_json_bool "$cfg" load_kick_enabled
+ uci_option_to_json_string "$cfg" node_up_script
+
+ for opt in \
+ debug_level \
+ sta_block_timeout local_sta_timeout local_sta_update \
+ max_retry_band seen_policy_timeout \
+ load_balancing_threshold band_steering_threshold \
+ remote_update_interval \
+ min_connect_snr min_snr signal_diff_threshold \
+ initial_connect_delay \
+ roam_kick_delay roam_scan_tries \
+ roam_scan_snr roam_scan_interval \
+ roam_trigger_snr roam_trigger_interval \
+ load_kick_threshold load_kick_delay load_kick_min_clients \
+ load_kick_reason_code
+ do
+ uci_option_to_json "$cfg" "$opt"
+ done
+}
+
+
+load_config() {
+ [ "$ENABLED" -gt 0 ] || return
+
+ ubus -t 10 wait_for usteer
+
+ json_init
+ json_add_array interfaces
+ for i in $(load_ifaces); do
+ json_add_string "" "$i"
+ done
+ json_close_array
+
+ config_load usteer
+ config_foreach uci_usteer usteer
+
+ ubus call usteer set_config "$(json_dump)"
+}
+
+reload_service() {
+ start
+ load_config
+}
+
+service_started() {
+ load_config
+}
+
+service_triggers() {
+ procd_add_reload_trigger usteer
+ procd_add_raw_trigger "interface.*" 2000 /etc/init.d/usteer reload
+}
+
+start_service()
+{
+ local network="$(uci -q get usteer.@usteer[-1].network)"
+ ENABLED="$(uci -q get usteer.@usteer[-1].enabled)"
+ ENABLED="${ENABLED:-1}"
+
+ [ "$ENABLED" -gt 0 ] || return
+
+ procd_open_instance
+ procd_set_param command "$PROG"
+ procd_close_instance
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include "usteer.h"
+#include "remote.h"
+
+bool parse_apmsg(struct apmsg *msg, struct blob_attr *data)
+{
+ static const struct blob_attr_info policy[__APMSG_MAX] = {
+ [APMSG_ID] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_SEQ] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODES] = { .type = BLOB_ATTR_NESTED },
+ };
+ struct blob_attr *tb[__APMSG_MAX];
+
+ blob_parse(data, tb, policy, __APMSG_MAX);
+ if (!tb[APMSG_ID] || !tb[APMSG_SEQ] || !tb[APMSG_NODES])
+ return false;
+
+ msg->id = blob_get_int32(tb[APMSG_ID]);
+ msg->seq = blob_get_int32(tb[APMSG_SEQ]);
+ msg->nodes = tb[APMSG_NODES];
+
+ return true;
+}
+
+static int
+get_int32(struct blob_attr *attr)
+{
+ if (!attr)
+ return 0;
+
+ return blob_get_int32(attr);
+}
+
+bool parse_apmsg_node(struct apmsg_node *msg, struct blob_attr *data)
+{
+ static const struct blob_attr_info policy[__APMSG_NODE_MAX] = {
+ [APMSG_NODE_NAME] = { .type = BLOB_ATTR_STRING },
+ [APMSG_NODE_FREQ] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODE_N_ASSOC] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODE_MAX_ASSOC] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODE_STATIONS] = { .type = BLOB_ATTR_NESTED },
+ [APMSG_NODE_NOISE] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODE_LOAD] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_NODE_RRM_NR] = { .type = BLOB_ATTR_NESTED },
+ [APMSG_NODE_SCRIPT_DATA] = { .type = BLOB_ATTR_NESTED },
+ };
+ struct blob_attr *tb[__APMSG_NODE_MAX];
+ struct blob_attr *cur;
+
+ blob_parse(data, tb, policy, __APMSG_NODE_MAX);
+ if (!tb[APMSG_NODE_NAME] ||
+ !tb[APMSG_NODE_FREQ] ||
+ !tb[APMSG_NODE_N_ASSOC] ||
+ !tb[APMSG_NODE_STATIONS] ||
+ !tb[APMSG_NODE_SSID])
+ return false;
+
+ msg->name = blob_data(tb[APMSG_NODE_NAME]);
+ msg->n_assoc = blob_get_int32(tb[APMSG_NODE_N_ASSOC]);
+ msg->freq = blob_get_int32(tb[APMSG_NODE_FREQ]);
+ msg->stations = tb[APMSG_NODE_STATIONS];
+ msg->ssid = blob_data(tb[APMSG_NODE_SSID]);
+
+ msg->noise = get_int32(tb[APMSG_NODE_NOISE]);
+ msg->load = get_int32(tb[APMSG_NODE_LOAD]);
+ msg->max_assoc = get_int32(tb[APMSG_NODE_MAX_ASSOC]);
+ msg->rrm_nr = NULL;
+
+ cur = tb[APMSG_NODE_RRM_NR];
+ if (cur && blob_len(cur) >= sizeof(struct blob_attr) &&
+ blob_len(cur) >= blob_pad_len(blob_data(cur))) {
+ int rem;
+
+ msg->rrm_nr = blob_data(cur);
+
+ blobmsg_for_each_attr(cur, msg->rrm_nr, rem) {
+ if (blobmsg_check_attr(cur, false))
+ continue;
+ if (blobmsg_type(cur) == BLOBMSG_TYPE_STRING)
+ continue;
+ msg->rrm_nr = NULL;
+ break;
+ }
+
+ if (msg->rrm_nr &&
+ blobmsg_type(msg->rrm_nr) != BLOBMSG_TYPE_ARRAY)
+ msg->rrm_nr = NULL;
+ }
+
+ msg->script_data = tb[APMSG_NODE_SCRIPT_DATA];
+
+ return true;
+}
+
+bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data)
+{
+ static const struct blob_attr_info policy[__APMSG_STA_MAX] = {
+ [APMSG_STA_ADDR] = { .type = BLOB_ATTR_BINARY },
+ [APMSG_STA_SIGNAL] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_STA_SEEN] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_STA_TIMEOUT] = { .type = BLOB_ATTR_INT32 },
+ [APMSG_STA_CONNECTED] = { .type = BLOB_ATTR_INT8 },
+ };
+ struct blob_attr *tb[__APMSG_STA_MAX];
+
+ blob_parse(data, tb, policy, __APMSG_STA_MAX);
+ if (!tb[APMSG_STA_ADDR] ||
+ !tb[APMSG_STA_SIGNAL] ||
+ !tb[APMSG_STA_SEEN] ||
+ !tb[APMSG_STA_TIMEOUT] ||
+ !tb[APMSG_STA_CONNECTED])
+ return false;
+
+ if (blob_len(tb[APMSG_STA_ADDR]) != sizeof(msg->addr))
+ return false;
+
+ memcpy(msg->addr, blob_data(tb[APMSG_STA_ADDR]), sizeof(msg->addr));
+ msg->signal = blob_get_int32(tb[APMSG_STA_SIGNAL]);
+ msg->seen = blob_get_int32(tb[APMSG_STA_SEEN]);
+ msg->timeout = blob_get_int32(tb[APMSG_STA_TIMEOUT]);
+ msg->connected = blob_get_int8(tb[APMSG_STA_CONNECTED]);
+
+ return true;
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include "usteer.h"
+#include "node.h"
+
+static bool
+below_assoc_threshold(struct sta_info *si_cur, struct sta_info *si_new)
+{
+ int n_assoc_cur = si_cur->node->n_assoc;
+ int n_assoc_new = si_new->node->n_assoc;
+ bool ref_5g = si_cur->node->freq > 4000;
+ bool node_5g = si_new->node->freq > 4000;
+
+ if (ref_5g && !node_5g)
+ n_assoc_new += config.band_steering_threshold;
+ else if (!ref_5g && node_5g)
+ n_assoc_cur += config.band_steering_threshold;
+
+ n_assoc_new += config.load_balancing_threshold;
+
+ if (n_assoc_new > n_assoc_cur) {
+ MSG_T_STA("band_steering_threshold,load_balancing_threshold",
+ si_cur->sta->addr, "exeeded (bs=%u, lb=%u)\n",
+ config.band_steering_threshold,
+ config.load_balancing_threshold);
+ }
+ return n_assoc_new <= n_assoc_cur;
+}
+
+static bool
+better_signal_strength(struct sta_info *si_cur, struct sta_info *si_new)
+{
+ const bool is_better = si_new->signal - si_cur->signal
+ > (int) config.signal_diff_threshold;
+
+ if (!config.signal_diff_threshold)
+ return false;
+
+ if (is_better) {
+ MSG_T_STA("signal_diff_threshold", si_cur->sta->addr,
+ "exceeded (config=%i) (real=%i)\n",
+ config.signal_diff_threshold,
+ si_new->signal - si_cur->signal);
+ }
+ return is_better;
+}
+
+static bool
+below_load_threshold(struct sta_info *si)
+{
+ return si->node->n_assoc >= config.load_kick_min_clients &&
+ si->node->load > config.load_kick_threshold;
+}
+
+static bool
+has_better_load(struct sta_info *si_cur, struct sta_info *si_new)
+{
+ return !below_load_threshold(si_cur) && below_load_threshold(si_new);
+}
+
+static bool
+below_max_assoc(struct sta_info *si)
+{
+ struct usteer_node *node = si->node;
+
+ return !node->max_assoc || node->n_assoc < node->max_assoc;
+}
+
+static bool
+is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
+{
+ if (!below_max_assoc(si_new))
+ return false;
+
+ return below_assoc_threshold(si_cur, si_new) ||
+ better_signal_strength(si_cur, si_new) ||
+ has_better_load(si_cur, si_new);
+}
+
+static struct sta_info *
+find_better_candidate(struct sta_info *si_ref)
+{
+ struct sta_info *si;
+ struct sta *sta = si_ref->sta;
+
+ list_for_each_entry(si, &sta->nodes, list) {
+ if (si == si_ref)
+ continue;
+
+ if (current_time - si->seen > config.seen_policy_timeout) {
+ MSG_T_STA("seen_policy_timeout", si->sta->addr,
+ "timeout exceeded (%u)\n", config.seen_policy_timeout);
+ continue;
+ }
+
+ if (strcmp(si->node->ssid, si_ref->node->ssid) != 0)
+ continue;
+
+ if (is_better_candidate(si_ref, si) &&
+ !is_better_candidate(si, si_ref))
+ return si;
+ }
+ return NULL;
+}
+
+static int
+snr_to_signal(struct usteer_node *node, int snr)
+{
+ int noise = -95;
+
+ if (snr < 0)
+ return snr;
+
+ if (node->noise)
+ noise = node->noise;
+
+ return noise + snr;
+}
+
+bool
+usteer_check_request(struct sta_info *si, enum usteer_event_type type)
+{
+ struct sta_info *si_new;
+ int min_signal;
+
+ if (type == EVENT_TYPE_ASSOC)
+ return true;
+
+ if (si->stats[type].blocked_cur >= config.max_retry_band) {
+ MSG_T_STA("max_retry_band", si->sta->addr,
+ "max retry (%u) exceeded\n", config.max_retry_band);
+ return true;
+ }
+
+ min_signal = snr_to_signal(si->node, config.min_connect_snr);
+ if (si->signal < min_signal) {
+ if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
+ MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT" due to low signal (%d < %d)\n",
+ event_types[type], MAC_ADDR_DATA(si->sta->addr),
+ si->signal, min_signal);
+ MSG_T_STA("min_connect_snr", si->sta->addr,
+ "snr to low (config=%i) (real=%i)\n",
+ min_signal, si->signal);
+ return false;
+ }
+
+ if (current_time - si->created < config.initial_connect_delay) {
+ if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
+ MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT" during initial connect delay\n",
+ event_types[type], MAC_ADDR_DATA(si->sta->addr));
+ MSG_T_STA("initial_connect_delay", si->sta->addr,
+ "is below delay (%u)\n", config.initial_connect_delay);
+ return false;
+ }
+
+ si_new = find_better_candidate(si);
+ if (!si_new)
+ return true;
+
+ if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
+ MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT", "
+ "node (local/remote): %s/%s, "
+ "signal=%d/%d, n_assoc=%d/%d\n", event_types[type],
+ MAC_ADDR_DATA(si->sta->addr),
+ usteer_node_name(si->node), usteer_node_name(si_new->node),
+ si->signal, si_new->signal,
+ si->node->n_assoc, si_new->node->n_assoc);
+
+ return false;
+}
+
+static bool
+is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
+{
+ if (!si_cur)
+ return true;
+
+ if (si_new->kick_count > si_cur->kick_count)
+ return false;
+
+ return si_cur->signal > si_new->signal;
+}
+
+static void
+usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state)
+{
+ static const char * const state_names[] = {
+#define _S(n) [ROAM_TRIGGER_##n] = #n,
+ __roam_trigger_states
+#undef _S
+ };
+
+ si->roam_event = current_time;
+
+ if (si->roam_state == state) {
+ if (si->roam_state == ROAM_TRIGGER_IDLE) {
+ si->roam_tries = 0;
+ return;
+ }
+
+ si->roam_tries++;
+ } else {
+ si->roam_tries = 0;
+ }
+
+ si->roam_state = state;
+
+ MSG(VERBOSE, "Roam trigger SM for client "MAC_ADDR_FMT": state=%s, tries=%d, signal=%d\n",
+ MAC_ADDR_DATA(si->sta->addr), state_names[state], si->roam_tries, si->signal);
+}
+
+static bool
+usteer_roam_trigger_sm(struct sta_info *si)
+{
+ struct sta_info *si_new;
+ int min_signal;
+
+ min_signal = snr_to_signal(si->node, config.roam_trigger_snr);
+
+ switch (si->roam_state) {
+ case ROAM_TRIGGER_SCAN:
+ if (current_time - si->roam_event < config.roam_scan_interval)
+ break;
+
+ if (find_better_candidate(si) ||
+ si->roam_scan_done > si->roam_event) {
+ usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE);
+ break;
+ }
+
+ if (config.roam_scan_tries &&
+ si->roam_tries >= config.roam_scan_tries) {
+ usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK);
+ break;
+ }
+
+ usteer_ubus_trigger_client_scan(si);
+ usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+ break;
+
+ case ROAM_TRIGGER_IDLE:
+ if (find_better_candidate(si)) {
+ usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE);
+ break;
+ }
+
+ usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+ break;
+
+ case ROAM_TRIGGER_SCAN_DONE:
+ /* Check for stale scan results, kick back to SCAN state if necessary */
+ if (current_time - si->roam_scan_done > 2 * config.roam_scan_interval) {
+ usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+ break;
+ }
+
+ si_new = find_better_candidate(si);
+ if (!si_new)
+ break;
+
+ usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK);
+ break;
+
+ case ROAM_TRIGGER_WAIT_KICK:
+ if (si->signal > min_signal)
+ break;
+
+ usteer_roam_set_state(si, ROAM_TRIGGER_NOTIFY_KICK);
+ usteer_ubus_notify_client_disassoc(si);
+ break;
+ case ROAM_TRIGGER_NOTIFY_KICK:
+ if (current_time - si->roam_event < config.roam_kick_delay * 100)
+ break;
+
+ usteer_roam_set_state(si, ROAM_TRIGGER_KICK);
+ break;
+ case ROAM_TRIGGER_KICK:
+ usteer_ubus_kick_client(si);
+ usteer_roam_set_state(si, ROAM_TRIGGER_IDLE);
+ return true;
+ }
+
+ return false;
+}
+
+static void
+usteer_local_node_roam_check(struct usteer_local_node *ln)
+{
+ struct sta_info *si;
+ int min_signal;
+
+ if (config.roam_scan_snr)
+ min_signal = config.roam_scan_snr;
+ else if (config.roam_trigger_snr)
+ min_signal = config.roam_trigger_snr;
+ else
+ return;
+
+ usteer_update_time();
+ min_signal = snr_to_signal(&ln->node, min_signal);
+
+ list_for_each_entry(si, &ln->node.sta_info, node_list) {
+ if (!si->connected || si->signal >= min_signal ||
+ current_time - si->roam_kick < config.roam_trigger_interval) {
+ usteer_roam_set_state(si, ROAM_TRIGGER_IDLE);
+ continue;
+ }
+
+ /*
+ * If the state machine kicked a client, other clients should wait
+ * until the next turn
+ */
+ if (usteer_roam_trigger_sm(si))
+ return;
+ }
+}
+
+static void
+usteer_local_node_snr_kick(struct usteer_local_node *ln)
+{
+ struct sta_info *si;
+ int min_signal;
+
+ if (!config.min_snr)
+ return;
+
+ min_signal = snr_to_signal(&ln->node, config.min_snr);
+
+ list_for_each_entry(si, &ln->node.sta_info, node_list) {
+ if (!si->connected)
+ continue;
+
+ if (si->signal >= min_signal)
+ continue;
+
+ si->kick_count++;
+
+ MSG(VERBOSE, "Kicking client "MAC_ADDR_FMT" due to low SNR, signal=%d\n",
+ MAC_ADDR_DATA(si->sta->addr), si->signal);
+
+ usteer_ubus_kick_client(si);
+ return;
+ }
+}
+
+void
+usteer_local_node_kick(struct usteer_local_node *ln)
+{
+ struct usteer_node *node = &ln->node;
+ struct sta_info *kick1 = NULL, *kick2 = NULL;
+ struct sta_info *candidate = NULL;
+ struct sta_info *si;
+
+ usteer_local_node_roam_check(ln);
+ usteer_local_node_snr_kick(ln);
+
+ if (!config.load_kick_enabled || !config.load_kick_threshold ||
+ !config.load_kick_delay)
+ return;
+
+ if (node->load < config.load_kick_threshold) {
+ MSG_T("load_kick_threshold",
+ "is below load for this node (config=%i) (real=%i)\n",
+ config.load_kick_threshold, node->load);
+ ln->load_thr_count = 0;
+ return;
+ }
+
+ if (++ln->load_thr_count <=
+ DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update)) {
+ MSG_T("load_kick_delay", "delay kicking (config=%i)\n",
+ config.load_kick_delay);
+ return;
+ }
+
+ MSG(VERBOSE, "AP load threshold exceeded on %s (%d), try to kick a client\n",
+ usteer_node_name(node), node->load);
+
+ ln->load_thr_count = 0;
+ if (node->n_assoc < config.load_kick_min_clients) {
+ MSG_T("load_kick_min_clients",
+ "min limit reached, stop kicking clients on this node "
+ "(n_assoc=%i) (config=%i)\n",
+ node->n_assoc, config.load_kick_min_clients);
+ return;
+ }
+
+ list_for_each_entry(si, &ln->node.sta_info, node_list) {
+ struct sta_info *tmp;
+
+ if (!si->connected)
+ continue;
+
+ if (is_more_kickable(kick1, si))
+ kick1 = si;
+
+ tmp = find_better_candidate(si);
+ if (!tmp)
+ continue;
+
+ if (is_more_kickable(kick2, si)) {
+ kick2 = si;
+ candidate = tmp;
+ }
+ }
+
+ if (!kick1)
+ return;
+
+ if (kick2)
+ kick1 = kick2;
+
+ MSG(VERBOSE, "Kicking client "MAC_ADDR_FMT", signal=%d, better_candidate=%s\n",
+ MAC_ADDR_DATA(kick1->sta->addr), kick1->signal,
+ candidate ? usteer_node_name(candidate->node) : "(none)");
+
+ kick1->kick_count++;
+ usteer_ubus_kick_client(kick1);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include <libubox/vlist.h>
+#include <libubox/avl-cmp.h>
+#include <libubox/usock.h>
+#include "usteer.h"
+#include "remote.h"
+#include "node.h"
+
+static uint32_t local_id;
+static struct uloop_fd remote_fd;
+static struct uloop_timeout remote_timer;
+static struct uloop_timeout reload_timer;
+
+static struct blob_buf buf;
+static uint32_t msg_seq;
+
+struct interface {
+ struct vlist_node node;
+ int ifindex;
+};
+
+static void
+interfaces_update_cb(struct vlist_tree *tree,
+ struct vlist_node *node_new,
+ struct vlist_node *node_old);
+
+static int remote_node_cmp(const void *k1, const void *k2, void *ptr)
+{
+ unsigned long v1 = (unsigned long) k1;
+ unsigned long v2 = (unsigned long) k2;
+
+ return v2 - v1;
+}
+
+static VLIST_TREE(interfaces, avl_strcmp, interfaces_update_cb, true, true);
+AVL_TREE(remote_nodes, remote_node_cmp, true, NULL);
+
+static const char *
+interface_name(struct interface *iface)
+{
+ return iface->node.avl.key;
+}
+
+static void
+interface_check(struct interface *iface)
+{
+ iface->ifindex = if_nametoindex(interface_name(iface));
+ uloop_timeout_set(&reload_timer, 1);
+}
+
+static void
+interface_init(struct interface *iface)
+{
+ interface_check(iface);
+}
+
+static void
+interface_free(struct interface *iface)
+{
+ avl_delete(&interfaces.avl, &iface->node.avl);
+ free(iface);
+}
+
+static void
+interfaces_update_cb(struct vlist_tree *tree,
+ struct vlist_node *node_new,
+ struct vlist_node *node_old)
+{
+ struct interface *iface;
+
+ if (node_new && node_old) {
+ iface = container_of(node_new, struct interface, node);
+ free(iface);
+ iface = container_of(node_old, struct interface, node);
+ interface_check(iface);
+ } else if (node_old) {
+ iface = container_of(node_old, struct interface, node);
+ interface_free(iface);
+ } else {
+ iface = container_of(node_new, struct interface, node);
+ interface_init(iface);
+ }
+}
+
+void usteer_interface_add(const char *name)
+{
+ struct interface *iface;
+ char *name_buf;
+
+ iface = calloc_a(sizeof(*iface), &name_buf, strlen(name) + 1);
+ strcpy(name_buf, name);
+ vlist_add(&interfaces, &iface->node, name_buf);
+}
+
+void config_set_interfaces(struct blob_attr *data)
+{
+ struct blob_attr *cur;
+ int rem;
+
+ if (!blobmsg_check_attr_list(data, BLOBMSG_TYPE_STRING))
+ return;
+
+ vlist_update(&interfaces);
+ blobmsg_for_each_attr(cur, data, rem) {
+ usteer_interface_add(blobmsg_data(cur));
+ }
+ vlist_flush(&interfaces);
+}
+
+void config_get_interfaces(struct blob_buf *buf)
+{
+ struct interface *iface;
+ void *c;
+
+ c = blobmsg_open_array(buf, "interfaces");
+ vlist_for_each_element(&interfaces, iface, node) {
+ blobmsg_add_string(buf, NULL, interface_name(iface));
+ }
+ blobmsg_close_array(buf, c);
+}
+
+static void
+interface_add_station(struct usteer_remote_node *node, struct blob_attr *data)
+{
+ struct sta *sta;
+ struct sta_info *si;
+ struct apmsg_sta msg;
+ bool create;
+
+ if (!parse_apmsg_sta(&msg, data)) {
+ MSG(DEBUG, "Cannot parse station in message\n");
+ return;
+ }
+
+ if (msg.timeout <= 0) {
+ MSG(DEBUG, "Refuse to add an already expired station entry\n");
+ return;
+ }
+
+ sta = usteer_sta_get(msg.addr, true);
+ if (!sta)
+ return;
+
+ si = usteer_sta_info_get(sta, &node->node, &create);
+ if (!si)
+ return;
+
+ si->connected = msg.connected;
+ si->signal = msg.signal;
+ si->seen = current_time - msg.seen;
+ usteer_sta_info_update_timeout(si, msg.timeout);
+}
+
+static void
+remote_node_free(struct usteer_remote_node *node)
+{
+ avl_delete(&remote_nodes, &node->avl);
+ usteer_sta_node_cleanup(&node->node);
+ free(node);
+}
+
+static struct usteer_remote_node *
+interface_get_node(const char *addr, unsigned long id, const char *name)
+{
+ struct usteer_remote_node *node;
+ int addr_len = strlen(addr);
+ char *buf;
+
+ node = avl_find_element(&remote_nodes, (void *) id, node, avl);
+ while (node && node->avl.key == (void *) id) {
+ if (!strcmp(node->name, name))
+ return node;
+
+ node = avl_next_element(node, avl);
+ }
+
+ node = calloc_a(sizeof(*node), &buf, addr_len + 1 + strlen(name) + 1);
+ node->avl.key = (void *) id;
+ node->node.type = NODE_TYPE_REMOTE;
+
+ sprintf(buf, "%s#%s", addr, name);
+ node->node.avl.key = buf;
+ node->name = buf + addr_len + 1;
+ INIT_LIST_HEAD(&node->node.sta_info);
+
+ avl_insert(&remote_nodes, &node->avl);
+
+ return node;
+}
+
+static void
+interface_add_node(struct interface *iface, const char *addr, unsigned long id, struct blob_attr *data)
+{
+ struct usteer_remote_node *node;
+ struct apmsg_node msg;
+ struct blob_attr *cur;
+ int rem;
+
+ if (!parse_apmsg_node(&msg, data)) {
+ MSG(DEBUG, "Cannot parse node in message\n");
+ return;
+ }
+
+ node = interface_get_node(addr, id, msg.name);
+ node->check = 0;
+ node->node.freq = msg.freq;
+ node->node.n_assoc = msg.n_assoc;
+ node->node.max_assoc = msg.max_assoc;
+ node->node.noise = msg.noise;
+ node->node.load = msg.load;
+ node->iface = iface;
+ snprintf(node->node.ssid, sizeof(node->node.ssid), "%s", msg.ssid);
+ usteer_node_set_blob(&node->node.rrm_nr, msg.rrm_nr);
+ usteer_node_set_blob(&node->node.script_data, msg.script_data);
+
+ blob_for_each_attr(cur, msg.stations, rem)
+ interface_add_station(node, cur);
+}
+
+static void
+interface_recv_msg(struct interface *iface, struct in_addr *addr, void *buf, int len)
+{
+ char addr_str[INET_ADDRSTRLEN];
+ struct blob_attr *data = buf;
+ struct apmsg msg;
+ struct blob_attr *cur;
+ int rem;
+
+ if (blob_pad_len(data) != len) {
+ MSG(DEBUG, "Invalid message length (header: %d, real: %d)\n", blob_pad_len(data), len);
+ return;
+ }
+
+ if (!parse_apmsg(&msg, data)) {
+ MSG(DEBUG, "Missing fields in message\n");
+ return;
+ }
+
+ if (msg.id == local_id)
+ return;
+
+ MSG(NETWORK, "Received message on %s (id=%08x->%08x seq=%d len=%d)\n",
+ interface_name(iface), msg.id, local_id, msg.seq, len);
+
+ inet_ntop(AF_INET, addr, addr_str, sizeof(addr_str));
+
+ blob_for_each_attr(cur, msg.nodes, rem)
+ interface_add_node(iface, addr_str, msg.id, cur);
+}
+
+static struct interface *
+interface_find_by_ifindex(int index)
+{
+ struct interface *iface;
+
+ vlist_for_each_element(&interfaces, iface, node) {
+ if (iface->ifindex == index)
+ return iface;
+ }
+
+ return NULL;
+}
+
+static void
+interface_recv(struct uloop_fd *u, unsigned int events)
+{
+ static char buf[APMGR_BUFLEN];
+ static char cmsg_buf[( CMSG_SPACE(sizeof(struct in_pktinfo)) + sizeof(int)) + 1];
+ static struct sockaddr_in sin;
+ static struct iovec iov = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf)
+ };
+ static struct msghdr msg = {
+ .msg_name = &sin,
+ .msg_namelen = sizeof(sin),
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cmsg_buf,
+ .msg_controllen = sizeof(cmsg_buf),
+ };
+ struct cmsghdr *cmsg;
+ int len;
+
+ do {
+ struct in_pktinfo *pkti = NULL;
+ struct interface *iface;
+
+ len = recvmsg(u->fd, &msg, 0);
+ if (len < 0) {
+ switch (errno) {
+ case EAGAIN:
+ return;
+ case EINTR:
+ continue;
+ default:
+ perror("recvmsg");
+ uloop_fd_delete(u);
+ return;
+ }
+ }
+
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_type != IP_PKTINFO)
+ continue;
+
+ pkti = (struct in_pktinfo *) CMSG_DATA(cmsg);
+ }
+
+ if (!pkti) {
+ MSG(DEBUG, "Received packet without ifindex\n");
+ continue;
+ }
+
+ iface = interface_find_by_ifindex(pkti->ipi_ifindex);
+ if (!iface) {
+ MSG(DEBUG, "Received packet from unconfigured interface %d\n", pkti->ipi_ifindex);
+ continue;
+ }
+
+ interface_recv_msg(iface, &sin.sin_addr, buf, len);
+ } while (1);
+}
+
+static void interface_send_msg(struct interface *iface, struct blob_attr *data)
+{
+ static size_t cmsg_data[( CMSG_SPACE(sizeof(struct in_pktinfo)) / sizeof(size_t)) + 1];
+ static struct sockaddr_in a;
+ static struct iovec iov;
+ static struct msghdr m = {
+ .msg_name = (struct sockaddr *) &a,
+ .msg_namelen = sizeof(a),
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cmsg_data,
+ .msg_controllen = CMSG_LEN(sizeof(struct in_pktinfo)),
+ };
+ struct in_pktinfo *pkti;
+ struct cmsghdr *cmsg;
+
+ a.sin_family = AF_INET;
+ a.sin_port = htons(16720);
+ a.sin_addr.s_addr = ~0;
+
+ memset(cmsg_data, 0, sizeof(cmsg_data));
+ cmsg = CMSG_FIRSTHDR(&m);
+ cmsg->cmsg_len = m.msg_controllen;
+ cmsg->cmsg_level = IPPROTO_IP;
+ cmsg->cmsg_type = IP_PKTINFO;
+
+ pkti = (struct in_pktinfo *) CMSG_DATA(cmsg);
+ pkti->ipi_ifindex = iface->ifindex;
+
+ iov.iov_base = data;
+ iov.iov_len = blob_pad_len(data);
+
+ if (sendmsg(remote_fd.fd, &m, 0) < 0)
+ perror("sendmsg");
+}
+
+static void usteer_send_sta_info(struct sta_info *sta)
+{
+ int seen = current_time - sta->seen;
+ void *c;
+
+ c = blob_nest_start(&buf, 0);
+ blob_put(&buf, APMSG_STA_ADDR, sta->sta->addr, 6);
+ blob_put_int8(&buf, APMSG_STA_CONNECTED, !!sta->connected);
+ blob_put_int32(&buf, APMSG_STA_SIGNAL, sta->signal);
+ blob_put_int32(&buf, APMSG_STA_SEEN, seen);
+ blob_put_int32(&buf, APMSG_STA_TIMEOUT, config.local_sta_timeout - seen);
+ blob_nest_end(&buf, c);
+}
+
+static void usteer_send_node(struct usteer_node *node, struct sta_info *sta)
+{
+ void *c, *s, *r;
+
+ c = blob_nest_start(&buf, 0);
+
+ blob_put_string(&buf, APMSG_NODE_NAME, usteer_node_name(node));
+ blob_put_string(&buf, APMSG_NODE_SSID, node->ssid);
+ blob_put_int32(&buf, APMSG_NODE_FREQ, node->freq);
+ blob_put_int32(&buf, APMSG_NODE_NOISE, node->noise);
+ blob_put_int32(&buf, APMSG_NODE_LOAD, node->load);
+ blob_put_int32(&buf, APMSG_NODE_N_ASSOC, node->n_assoc);
+ blob_put_int32(&buf, APMSG_NODE_MAX_ASSOC, node->max_assoc);
+ if (node->rrm_nr) {
+ r = blob_nest_start(&buf, APMSG_NODE_RRM_NR);
+ blobmsg_add_field(&buf, BLOBMSG_TYPE_ARRAY, "",
+ blobmsg_data(node->rrm_nr),
+ blobmsg_data_len(node->rrm_nr));
+ blob_nest_end(&buf, r);
+ }
+
+ if (node->script_data)
+ blob_put(&buf, APMSG_NODE_SCRIPT_DATA,
+ blob_data(node->script_data),
+ blob_len(node->script_data));
+
+ s = blob_nest_start(&buf, APMSG_NODE_STATIONS);
+
+ if (sta) {
+ usteer_send_sta_info(sta);
+ } else {
+ list_for_each_entry(sta, &node->sta_info, node_list)
+ usteer_send_sta_info(sta);
+ }
+
+ blob_nest_end(&buf, s);
+
+ blob_nest_end(&buf, c);
+}
+
+static void
+usteer_check_timeout(void)
+{
+ struct usteer_remote_node *node, *tmp;
+ int timeout = config.remote_node_timeout / config.remote_update_interval;
+
+ avl_for_each_element_safe(&remote_nodes, node, avl, tmp) {
+ if (node->check++ > timeout)
+ remote_node_free(node);
+ }
+}
+
+static void *
+usteer_update_init(void)
+{
+ blob_buf_init(&buf, 0);
+ blob_put_int32(&buf, APMSG_ID, local_id);
+ blob_put_int32(&buf, APMSG_SEQ, ++msg_seq);
+
+ return blob_nest_start(&buf, APMSG_NODES);
+}
+
+static void
+usteer_update_send(void *c)
+{
+ struct interface *iface;
+
+ blob_nest_end(&buf, c);
+
+ vlist_for_each_element(&interfaces, iface, node)
+ interface_send_msg(iface, buf.head);
+}
+
+void
+usteer_send_sta_update(struct sta_info *si)
+{
+ void *c = usteer_update_init();
+ usteer_send_node(si->node, si);
+ usteer_update_send(c);
+}
+
+static void
+usteer_send_update_timer(struct uloop_timeout *t)
+{
+ struct usteer_node *node;
+ void *c;
+
+ MSG_T("remote_update_interval", "start remote update (interval=%u)\n",
+ config.remote_update_interval);
+
+ usteer_update_time();
+ uloop_timeout_set(t, config.remote_update_interval);
+
+ c = usteer_update_init();
+ avl_for_each_element(&local_nodes, node, avl)
+ usteer_send_node(node, NULL);
+
+ usteer_update_send(c);
+ usteer_check_timeout();
+}
+
+static int
+usteer_init_local_id(void)
+{
+ FILE *f;
+
+ f = fopen("/dev/urandom", "r");
+ if (!f) {
+ perror("fopen(/dev/urandom)");
+ return -1;
+ }
+
+ if (fread(&local_id, sizeof(local_id), 1, f) < 1)
+ return -1;
+
+ fclose(f);
+ return 0;
+}
+
+static void
+usteer_reload_timer(struct uloop_timeout *t)
+{
+ int yes = 1;
+ int fd;
+
+ if (remote_fd.registered) {
+ uloop_fd_delete(&remote_fd);
+ close(remote_fd.fd);
+ }
+
+ fd = usock(USOCK_UDP | USOCK_SERVER | USOCK_NONBLOCK |
+ USOCK_NUMERIC | USOCK_IPV4ONLY,
+ "0.0.0.0", APMGR_PORT_STR);
+ if (fd < 0) {
+ perror("usock");
+ return;
+ }
+
+ if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &yes, sizeof(yes)) < 0)
+ perror("setsockopt(IP_PKTINFO)");
+
+ if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes)) < 0)
+ perror("setsockopt(SO_BROADCAST)");
+
+ remote_fd.fd = fd;
+ remote_fd.cb = interface_recv;
+ uloop_fd_add(&remote_fd, ULOOP_READ);
+}
+
+int usteer_interface_init(void)
+{
+ if (usteer_init_local_id())
+ return -1;
+
+ remote_timer.cb = usteer_send_update_timer;
+ remote_timer.cb(&remote_timer);
+
+ reload_timer.cb = usteer_reload_timer;
+ reload_timer.cb(&reload_timer);
+
+ return 0;
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#ifndef __APMGR_REMOTE_H
+#define __APMGR_REMOTE_H
+
+#include <libubox/blob.h>
+
+enum {
+ APMSG_ID,
+ APMSG_SEQ,
+ APMSG_NODES,
+ __APMSG_MAX
+};
+
+struct apmsg {
+ uint32_t id;
+ uint32_t seq;
+ struct blob_attr *nodes;
+};
+
+enum {
+ APMSG_NODE_NAME,
+ APMSG_NODE_FREQ,
+ APMSG_NODE_N_ASSOC,
+ APMSG_NODE_STATIONS,
+ APMSG_NODE_NOISE,
+ APMSG_NODE_LOAD,
+ APMSG_NODE_SSID,
+ APMSG_NODE_MAX_ASSOC,
+ APMSG_NODE_RRM_NR,
+ APMSG_NODE_SCRIPT_DATA,
+ __APMSG_NODE_MAX
+};
+
+struct apmsg_node {
+ const char *name;
+ const char *ssid;
+ int freq;
+ int n_assoc;
+ int max_assoc;
+ int noise;
+ int load;
+ struct blob_attr *stations;
+ struct blob_attr *rrm_nr;
+ struct blob_attr *script_data;
+};
+
+enum {
+ APMSG_STA_ADDR,
+ APMSG_STA_SIGNAL,
+ APMSG_STA_TIMEOUT,
+ APMSG_STA_SEEN,
+ APMSG_STA_CONNECTED,
+ __APMSG_STA_MAX
+};
+
+struct apmsg_sta {
+ uint8_t addr[6];
+
+ bool connected;
+ int signal;
+ int timeout;
+ int seen;
+};
+
+bool parse_apmsg(struct apmsg *msg, struct blob_attr *data);
+bool parse_apmsg_node(struct apmsg_node *msg, struct blob_attr *data);
+bool parse_apmsg_sta(struct apmsg_sta *msg, struct blob_attr *data);
+
+#endif
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include "usteer.h"
+
+static int
+avl_macaddr_cmp(const void *k1, const void *k2, void *ptr)
+{
+ return memcmp(k1, k2, 6);
+}
+
+AVL_TREE(stations, avl_macaddr_cmp, false, NULL);
+static struct usteer_timeout_queue tq;
+
+static void
+usteer_sta_del(struct sta *sta)
+{
+ MSG(DEBUG, "Delete station " MAC_ADDR_FMT "\n",
+ MAC_ADDR_DATA(sta->addr));
+
+ avl_delete(&stations, &sta->avl);
+ free(sta);
+}
+
+static void
+usteer_sta_info_del(struct sta_info *si)
+{
+ struct sta *sta = si->sta;
+
+ MSG(DEBUG, "Delete station " MAC_ADDR_FMT " entry for node %s\n",
+ MAC_ADDR_DATA(sta->addr), usteer_node_name(si->node));
+
+ usteer_timeout_cancel(&tq, &si->timeout);
+ list_del(&si->list);
+ list_del(&si->node_list);
+ free(si);
+
+ if (list_empty(&sta->nodes))
+ usteer_sta_del(sta);
+}
+
+void
+usteer_sta_node_cleanup(struct usteer_node *node)
+{
+ struct sta_info *si, *tmp;
+
+ free(node->rrm_nr);
+ node->rrm_nr = NULL;
+
+ list_for_each_entry_safe(si, tmp, &node->sta_info, node_list)
+ usteer_sta_info_del(si);
+}
+
+static void
+usteer_sta_info_timeout(struct usteer_timeout_queue *q, struct usteer_timeout *t)
+{
+ struct sta_info *si = container_of(t, struct sta_info, timeout);
+
+ MSG_T_STA("local_sta_timeout", si->sta->addr,
+ "timeout expired, deleting sta info\n");
+
+ usteer_sta_info_del(si);
+}
+
+struct sta_info *
+usteer_sta_info_get(struct sta *sta, struct usteer_node *node, bool *create)
+{
+ struct sta_info *si;
+
+ list_for_each_entry(si, &sta->nodes, list) {
+ if (si->node != node)
+ continue;
+
+ if (create)
+ *create = false;
+
+ return si;
+ }
+
+ if (!create)
+ return NULL;
+
+ MSG(DEBUG, "Create station " MAC_ADDR_FMT " entry for node %s\n",
+ MAC_ADDR_DATA(sta->addr), usteer_node_name(node));
+
+ si = calloc(1, sizeof(*si));
+ si->node = node;
+ si->sta = sta;
+ list_add(&si->list, &sta->nodes);
+ list_add(&si->node_list, &node->sta_info);
+ si->created = current_time;
+ *create = true;
+
+ return si;
+}
+
+
+void
+usteer_sta_info_update_timeout(struct sta_info *si, int timeout)
+{
+ if (si->connected == 1)
+ usteer_timeout_cancel(&tq, &si->timeout);
+ else if (timeout > 0)
+ usteer_timeout_set(&tq, &si->timeout, timeout);
+ else
+ usteer_sta_info_del(si);
+}
+
+struct sta *
+usteer_sta_get(const uint8_t *addr, bool create)
+{
+ struct sta *sta;
+
+ sta = avl_find_element(&stations, addr, sta, avl);
+ if (sta)
+ return sta;
+
+ if (!create)
+ return NULL;
+
+ MSG(DEBUG, "Create station entry " MAC_ADDR_FMT "\n", MAC_ADDR_DATA(addr));
+ sta = calloc(1, sizeof(*sta));
+ memcpy(sta->addr, addr, sizeof(sta->addr));
+ sta->avl.key = sta->addr;
+ avl_insert(&stations, &sta->avl);
+ INIT_LIST_HEAD(&sta->nodes);
+
+ return sta;
+}
+
+void
+usteer_sta_info_update(struct sta_info *si, int signal, bool avg)
+{
+ /* ignore probe request signal when connected */
+ if (si->connected == 1 && si->signal != NO_SIGNAL && !avg)
+ signal = NO_SIGNAL;
+
+ if (signal != NO_SIGNAL)
+ si->signal = signal;
+
+ si->seen = current_time;
+ usteer_sta_info_update_timeout(si, config.local_sta_timeout);
+}
+
+bool
+usteer_handle_sta_event(struct usteer_node *node, const uint8_t *addr,
+ enum usteer_event_type type, int freq, int signal)
+{
+ struct sta *sta;
+ struct sta_info *si;
+ uint32_t diff;
+ bool ret;
+ bool create;
+
+ sta = usteer_sta_get(addr, true);
+ if (!sta)
+ return -1;
+
+ if (freq < 4000)
+ sta->seen_2ghz = 1;
+ else
+ sta->seen_5ghz = 1;
+
+ si = usteer_sta_info_get(sta, node, &create);
+ usteer_sta_info_update(si, signal, false);
+ si->roam_scan_done = current_time;
+ si->stats[type].requests++;
+
+ diff = si->stats[type].blocked_last_time - current_time;
+ if (diff > config.sta_block_timeout) {
+ si->stats[type].blocked_cur = 0;
+ MSG_T_STA("sta_block_timeout", addr, "timeout expired\n");
+ }
+
+ ret = usteer_check_request(si, type);
+ if (!ret) {
+ si->stats[type].blocked_cur++;
+ si->stats[type].blocked_total++;
+ si->stats[type].blocked_last_time = current_time;
+ } else {
+ si->stats[type].blocked_cur = 0;
+ }
+
+ if (create)
+ usteer_send_sta_update(si);
+
+ return ret;
+}
+
+static void __usteer_init usteer_sta_init(void)
+{
+ usteer_timeout_init(&tq);
+ tq.cb = usteer_sta_info_timeout;
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <string.h>
+
+#include <libubox/utils.h>
+
+#include "timeout.h"
+
+static int usteer_timeout_cmp(const void *k1, const void *k2, void *ptr)
+{
+ uint32_t ref = (uint32_t) (intptr_t) ptr;
+ int32_t t1 = (uint32_t) (intptr_t) k1 - ref;
+ int32_t t2 = (uint32_t) (intptr_t) k2 - ref;
+
+ if (t1 < t2)
+ return -1;
+ else if (t1 > t2)
+ return 1;
+ else
+ return 0;
+}
+
+static int32_t usteer_timeout_delta(struct usteer_timeout *t, uint32_t time)
+{
+ uint32_t val = (uint32_t) (intptr_t) t->node.key;
+ return val - time;
+}
+
+static void usteer_timeout_recalc(struct usteer_timeout_queue *q, uint32_t time)
+{
+ struct usteer_timeout *t;
+ int32_t delta;
+
+ if (avl_is_empty(&q->tree)) {
+ uloop_timeout_cancel(&q->timeout);
+ return;
+ }
+
+ t = avl_first_element(&q->tree, t, node);
+
+ delta = usteer_timeout_delta(t, time);
+ if (delta < 1)
+ delta = 1;
+
+ uloop_timeout_set(&q->timeout, delta);
+}
+
+static uint32_t ampgr_timeout_current_time(void)
+{
+ struct timespec ts;
+ uint32_t val;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ val = ts.tv_sec * 1000;
+ val += ts.tv_nsec / 1000000;
+
+ return val;
+}
+
+static void usteer_timeout_cb(struct uloop_timeout *timeout)
+{
+ struct usteer_timeout_queue *q;
+ struct usteer_timeout *t, *tmp;
+ bool found;
+ uint32_t time;
+
+ q = container_of(timeout, struct usteer_timeout_queue, timeout);
+ do {
+ found = false;
+ time = ampgr_timeout_current_time();
+
+ avl_for_each_element_safe(&q->tree, t, node, tmp) {
+ if (usteer_timeout_delta(t, time) > 0)
+ break;
+
+ usteer_timeout_cancel(q, t);
+ if (q->cb)
+ q->cb(q, t);
+ found = true;
+ }
+ } while (found);
+
+ usteer_timeout_recalc(q, time);
+}
+
+
+void usteer_timeout_init(struct usteer_timeout_queue *q)
+{
+ avl_init(&q->tree, usteer_timeout_cmp, true, NULL);
+ q->timeout.cb = usteer_timeout_cb;
+}
+
+static void __usteer_timeout_cancel(struct usteer_timeout_queue *q,
+ struct usteer_timeout *t)
+{
+ avl_delete(&q->tree, &t->node);
+}
+
+void usteer_timeout_set(struct usteer_timeout_queue *q, struct usteer_timeout *t,
+ int msecs)
+{
+ uint32_t time = ampgr_timeout_current_time();
+ uint32_t val = time + msecs;
+ bool recalc = false;
+
+ q->tree.cmp_ptr = (void *) (intptr_t) time;
+ if (usteer_timeout_isset(t)) {
+ if (avl_is_first(&q->tree, &t->node))
+ recalc = true;
+
+ __usteer_timeout_cancel(q, t);
+ }
+
+ t->node.key = (void *) (intptr_t) val;
+ avl_insert(&q->tree, &t->node);
+ if (avl_is_first(&q->tree, &t->node))
+ recalc = true;
+
+ if (recalc)
+ usteer_timeout_recalc(q, time);
+}
+
+void usteer_timeout_cancel(struct usteer_timeout_queue *q,
+ struct usteer_timeout *t)
+{
+ if (!usteer_timeout_isset(t))
+ return;
+
+ __usteer_timeout_cancel(q, t);
+ memset(&t->node.list, 0, sizeof(t->node.list));
+}
+
+void usteer_timeout_flush(struct usteer_timeout_queue *q)
+{
+ struct usteer_timeout *t, *tmp;
+
+ uloop_timeout_cancel(&q->timeout);
+ avl_remove_all_elements(&q->tree, t, node, tmp) {
+ memset(&t->node.list, 0, sizeof(t->node.list));
+ if (q->cb)
+ q->cb(q, t);
+ }
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#ifndef __APMGR_TIMEOUT_H
+#define __APMGR_TIMEOUT_H
+
+#include <libubox/avl.h>
+#include <libubox/uloop.h>
+
+struct usteer_timeout {
+ struct avl_node node;
+};
+
+struct usteer_timeout_queue {
+ struct avl_tree tree;
+ struct uloop_timeout timeout;
+ void (*cb)(struct usteer_timeout_queue *q, struct usteer_timeout *t);
+};
+
+static inline bool
+usteer_timeout_isset(struct usteer_timeout *t)
+{
+ return t->node.list.prev != NULL;
+}
+
+void usteer_timeout_init(struct usteer_timeout_queue *q);
+void usteer_timeout_set(struct usteer_timeout_queue *q, struct usteer_timeout *t,
+ int msecs);
+void usteer_timeout_cancel(struct usteer_timeout_queue *q,
+ struct usteer_timeout *t);
+void usteer_timeout_flush(struct usteer_timeout_queue *q);
+
+#endif
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/ethernet.h>
+#ifdef linux
+#include <netinet/ether.h>
+#endif
+
+#include "usteer.h"
+#include "node.h"
+
+static struct blob_buf b;
+
+static int
+usteer_ubus_get_clients(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct sta_info *si;
+ struct sta *sta;
+ char str[20];
+ void *_s, *_cur_n;
+
+ blob_buf_init(&b, 0);
+ avl_for_each_element(&stations, sta, avl) {
+ sprintf(str, MAC_ADDR_FMT, MAC_ADDR_DATA(sta->addr));
+ _s = blobmsg_open_table(&b, str);
+ list_for_each_entry(si, &sta->nodes, list) {
+ _cur_n = blobmsg_open_table(&b, usteer_node_name(si->node));
+ blobmsg_add_u8(&b, "connected", si->connected);
+ blobmsg_add_u32(&b, "signal", si->signal);
+ blobmsg_close_table(&b, _cur_n);
+ }
+ blobmsg_close_table(&b, _s);
+ }
+ ubus_send_reply(ctx, req, b.head);
+ return 0;
+}
+
+static struct blobmsg_policy client_arg[] = {
+ { .name = "address", .type = BLOBMSG_TYPE_STRING, },
+};
+
+static void
+usteer_ubus_add_stats(struct sta_info_stats *stats, const char *name)
+{
+ void *s;
+
+ s = blobmsg_open_table(&b, name);
+ blobmsg_add_u32(&b, "requests", stats->requests);
+ blobmsg_add_u32(&b, "blocked_cur", stats->blocked_cur);
+ blobmsg_add_u32(&b, "blocked_total", stats->blocked_total);
+ blobmsg_close_table(&b, s);
+}
+
+static int
+usteer_ubus_get_client_info(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct sta_info *si;
+ struct sta *sta;
+ struct blob_attr *mac_str;
+ uint8_t *mac;
+ void *_n, *_cur_n, *_s;
+ int i;
+
+ blobmsg_parse(client_arg, 1, &mac_str, blob_data(msg), blob_len(msg));
+ if (!mac_str)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ mac = (uint8_t *) ether_aton(blobmsg_data(mac_str));
+ if (!mac)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ sta = usteer_sta_get(mac, false);
+ if (!sta)
+ return UBUS_STATUS_NOT_FOUND;
+
+ blob_buf_init(&b, 0);
+ blobmsg_add_u8(&b, "2ghz", sta->seen_2ghz);
+ blobmsg_add_u8(&b, "5ghz", sta->seen_5ghz);
+ _n = blobmsg_open_table(&b, "nodes");
+ list_for_each_entry(si, &sta->nodes, list) {
+ _cur_n = blobmsg_open_table(&b, usteer_node_name(si->node));
+ blobmsg_add_u8(&b, "connected", si->connected);
+ blobmsg_add_u32(&b, "signal", si->signal);
+ _s = blobmsg_open_table(&b, "stats");
+ for (i = 0; i < __EVENT_TYPE_MAX; i++)
+ usteer_ubus_add_stats(&si->stats[EVENT_TYPE_PROBE], event_types[i]);
+ blobmsg_close_table(&b, _s);
+ blobmsg_close_table(&b, _cur_n);
+ }
+ blobmsg_close_table(&b, _n);
+
+ ubus_send_reply(ctx, req, b.head);
+
+ return 0;
+}
+
+enum cfg_type {
+ CFG_BOOL,
+ CFG_I32,
+ CFG_U32,
+ CFG_ARRAY_CB,
+ CFG_STRING_CB,
+};
+
+struct cfg_item {
+ enum cfg_type type;
+ union {
+ bool *BOOL;
+ uint32_t *U32;
+ int32_t *I32;
+ struct {
+ void (*set)(struct blob_attr *data);
+ void (*get)(struct blob_buf *buf);
+ } CB;
+ } ptr;
+};
+
+#define __config_items \
+ _cfg(BOOL, syslog), \
+ _cfg(U32, debug_level), \
+ _cfg(U32, sta_block_timeout), \
+ _cfg(U32, local_sta_timeout), \
+ _cfg(U32, local_sta_update), \
+ _cfg(U32, max_retry_band), \
+ _cfg(U32, seen_policy_timeout), \
+ _cfg(U32, load_balancing_threshold), \
+ _cfg(U32, band_steering_threshold), \
+ _cfg(U32, remote_update_interval), \
+ _cfg(I32, min_connect_snr), \
+ _cfg(I32, min_snr), \
+ _cfg(I32, roam_scan_snr), \
+ _cfg(U32, roam_scan_tries), \
+ _cfg(U32, roam_scan_interval), \
+ _cfg(I32, roam_trigger_snr), \
+ _cfg(U32, roam_trigger_interval), \
+ _cfg(U32, roam_kick_delay), \
+ _cfg(U32, signal_diff_threshold), \
+ _cfg(U32, initial_connect_delay), \
+ _cfg(BOOL, load_kick_enabled), \
+ _cfg(U32, load_kick_threshold), \
+ _cfg(U32, load_kick_delay), \
+ _cfg(U32, load_kick_min_clients), \
+ _cfg(U32, load_kick_reason_code), \
+ _cfg(ARRAY_CB, interfaces), \
+ _cfg(STRING_CB, node_up_script)
+
+enum cfg_items {
+#define _cfg(_type, _name) CFG_##_name
+ __config_items,
+#undef _cfg
+ __CFG_MAX,
+};
+
+static const struct blobmsg_policy config_policy[__CFG_MAX] = {
+#define _cfg_policy(_type, _name) [CFG_##_name] = { .name = #_name, .type = BLOBMSG_TYPE_ ## _type }
+#define _cfg_policy_BOOL(_name) _cfg_policy(BOOL, _name)
+#define _cfg_policy_U32(_name) _cfg_policy(INT32, _name)
+#define _cfg_policy_I32(_name) _cfg_policy(INT32, _name)
+#define _cfg_policy_ARRAY_CB(_name) _cfg_policy(ARRAY, _name)
+#define _cfg_policy_STRING_CB(_name) _cfg_policy(STRING, _name)
+#define _cfg(_type, _name) _cfg_policy_##_type(_name)
+ __config_items,
+#undef _cfg
+};
+
+static const struct cfg_item config_data[__CFG_MAX] = {
+#define _cfg_data_BOOL(_name) .ptr.BOOL = &config._name
+#define _cfg_data_U32(_name) .ptr.U32 = &config._name
+#define _cfg_data_I32(_name) .ptr.I32 = &config._name
+#define _cfg_data_ARRAY_CB(_name) .ptr.CB = { .set = config_set_##_name, .get = config_get_##_name }
+#define _cfg_data_STRING_CB(_name) .ptr.CB = { .set = config_set_##_name, .get = config_get_##_name }
+#define _cfg(_type, _name) [CFG_##_name] = { .type = CFG_##_type, _cfg_data_##_type(_name) }
+ __config_items,
+#undef _cfg
+};
+
+static int
+usteer_ubus_get_config(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int i;
+
+ blob_buf_init(&b, 0);
+ for (i = 0; i < __CFG_MAX; i++) {
+ switch(config_data[i].type) {
+ case CFG_BOOL:
+ blobmsg_add_u8(&b, config_policy[i].name,
+ *config_data[i].ptr.BOOL);
+ break;
+ case CFG_I32:
+ case CFG_U32:
+ blobmsg_add_u32(&b, config_policy[i].name,
+ *config_data[i].ptr.U32);
+ break;
+ case CFG_ARRAY_CB:
+ case CFG_STRING_CB:
+ config_data[i].ptr.CB.get(&b);
+ break;
+ }
+ }
+ ubus_send_reply(ctx, req, b.head);
+ return 0;
+}
+
+static int
+usteer_ubus_set_config(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct blob_attr *tb[__CFG_MAX];
+ int i;
+
+ if (!strcmp(method, "set_config"))
+ usteer_init_defaults();
+
+ blobmsg_parse(config_policy, __CFG_MAX, tb, blob_data(msg), blob_len(msg));
+ for (i = 0; i < __CFG_MAX; i++) {
+ if (!tb[i])
+ continue;
+
+ switch(config_data[i].type) {
+ case CFG_BOOL:
+ *config_data[i].ptr.BOOL = blobmsg_get_u8(tb[i]);
+ break;
+ case CFG_I32:
+ case CFG_U32:
+ *config_data[i].ptr.U32 = blobmsg_get_u32(tb[i]);
+ break;
+ case CFG_ARRAY_CB:
+ case CFG_STRING_CB:
+ config_data[i].ptr.CB.set(tb[i]);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static void
+usteer_dump_node_info(struct usteer_node *node)
+{
+ void *c;
+
+ c = blobmsg_open_table(&b, usteer_node_name(node));
+ blobmsg_add_u32(&b, "freq", node->freq);
+ blobmsg_add_u32(&b, "n_assoc", node->n_assoc);
+ blobmsg_add_u32(&b, "noise", node->noise);
+ blobmsg_add_u32(&b, "load", node->load);
+ blobmsg_add_u32(&b, "max_assoc", node->max_assoc);
+ if (node->rrm_nr)
+ blobmsg_add_field(&b, BLOBMSG_TYPE_ARRAY, "rrm_nr",
+ blobmsg_data(node->rrm_nr),
+ blobmsg_data_len(node->rrm_nr));
+ blobmsg_close_table(&b, c);
+}
+
+static int
+usteer_ubus_local_info(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct usteer_node *node;
+
+ blob_buf_init(&b, 0);
+
+ avl_for_each_element(&local_nodes, node, avl)
+ usteer_dump_node_info(node);
+
+ ubus_send_reply(ctx, req, b.head);
+
+ return 0;
+}
+
+static int
+usteer_ubus_remote_info(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct usteer_remote_node *rn;
+
+ blob_buf_init(&b, 0);
+
+ avl_for_each_element(&remote_nodes, rn, avl)
+ usteer_dump_node_info(&rn->node);
+
+ ubus_send_reply(ctx, req, b.head);
+
+ return 0;
+}
+
+static const struct ubus_method usteer_methods[] = {
+ UBUS_METHOD_NOARG("local_info", usteer_ubus_local_info),
+ UBUS_METHOD_NOARG("remote_info", usteer_ubus_remote_info),
+ UBUS_METHOD_NOARG("get_clients", usteer_ubus_get_clients),
+ UBUS_METHOD("get_client_info", usteer_ubus_get_client_info, client_arg),
+ UBUS_METHOD_NOARG("get_config", usteer_ubus_get_config),
+ UBUS_METHOD("set_config", usteer_ubus_set_config, config_policy),
+ UBUS_METHOD("update_config", usteer_ubus_set_config, config_policy),
+};
+
+static struct ubus_object_type usteer_obj_type =
+ UBUS_OBJECT_TYPE("usteer", usteer_methods);
+
+static struct ubus_object usteer_obj = {
+ .name = "usteer",
+ .type = &usteer_obj_type,
+ .methods = usteer_methods,
+ .n_methods = ARRAY_SIZE(usteer_methods),
+};
+
+static void
+usteer_add_nr_entry(struct usteer_node *ln, struct usteer_node *node)
+{
+ struct blobmsg_policy policy[3] = {
+ { .type = BLOBMSG_TYPE_STRING },
+ { .type = BLOBMSG_TYPE_STRING },
+ { .type = BLOBMSG_TYPE_STRING },
+ };
+ struct blob_attr *tb[3];
+
+ if (!node->rrm_nr)
+ return;
+
+ if (strcmp(ln->ssid, node->ssid) != 0)
+ return;
+
+ blobmsg_parse_array(policy, ARRAY_SIZE(tb), tb,
+ blobmsg_data(node->rrm_nr),
+ blobmsg_data_len(node->rrm_nr));
+ if (!tb[2])
+ return;
+
+ blobmsg_add_field(&b, BLOBMSG_TYPE_STRING, "",
+ blobmsg_data(tb[2]),
+ blobmsg_data_len(tb[2]));
+}
+
+int usteer_ubus_notify_client_disassoc(struct sta_info *si)
+{
+ struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+ struct usteer_remote_node *rn;
+ struct usteer_node *node;
+ void *c;
+
+ blob_buf_init(&b, 0);
+ blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
+ blobmsg_add_u32(&b, "duration", config.roam_kick_delay);
+ c = blobmsg_open_array(&b, "neighbors");
+ avl_for_each_element(&local_nodes, node, avl)
+ usteer_add_nr_entry(si->node, node);
+ avl_for_each_element(&remote_nodes, rn, avl)
+ usteer_add_nr_entry(si->node, &rn->node);
+ blobmsg_close_array(&b, c);
+ return ubus_invoke(ubus_ctx, ln->obj_id, "wnm_disassoc_imminent", b.head, NULL, 0, 100);
+}
+
+int usteer_ubus_trigger_client_scan(struct sta_info *si)
+{
+ struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+
+ si->scan_band = !si->scan_band;
+
+ MSG_T_STA("load_kick_reason_code", si->sta->addr,
+ "tell hostapd to issue a client beacon request (5ghz: %d)\n",
+ si->scan_band);
+
+ blob_buf_init(&b, 0);
+ blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
+ blobmsg_add_u32(&b, "mode", 1);
+ blobmsg_add_u32(&b, "duration", 65535);
+ blobmsg_add_u32(&b, "channel", 255);
+ blobmsg_add_u32(&b, "op_class", si->scan_band ? 1 : 12);
+ return ubus_invoke(ubus_ctx, ln->obj_id, "rrm_beacon_req", b.head, NULL, 0, 100);
+}
+
+void usteer_ubus_kick_client(struct sta_info *si)
+{
+ struct usteer_local_node *ln = container_of(si->node, struct usteer_local_node, node);
+
+ MSG_T_STA("load_kick_reason_code", si->sta->addr,
+ "tell hostapd to kick client with reason code %u\n",
+ config.load_kick_reason_code);
+
+ blob_buf_init(&b, 0);
+ blobmsg_printf(&b, "addr", MAC_ADDR_FMT, MAC_ADDR_DATA(si->sta->addr));
+ blobmsg_add_u32(&b, "reason", config.load_kick_reason_code);
+ blobmsg_add_u8(&b, "deauth", 1);
+ ubus_invoke(ubus_ctx, ln->obj_id, "del_client", b.head, NULL, 0, 100);
+ si->connected = 0;
+ si->roam_kick = current_time;
+}
+
+void usteer_ubus_init(struct ubus_context *ctx)
+{
+ ubus_add_object(ctx, &usteer_obj);
+}
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#ifndef __APMGR_H
+#define __APMGR_H
+
+#include <libubox/avl.h>
+#include <libubox/blobmsg.h>
+#include <libubox/uloop.h>
+#include <libubox/utils.h>
+#include <libubus.h>
+#include "utils.h"
+#include "timeout.h"
+
+#define NO_SIGNAL 0xff
+
+#define __STR(x) #x
+#define _STR(x) __STR(x)
+#define APMGR_PORT 16720 /* AP */
+#define APMGR_PORT_STR _STR(APMGR_PORT)
+#define APMGR_BUFLEN (64 * 1024)
+
+#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))
+
+enum usteer_event_type {
+ EVENT_TYPE_PROBE,
+ EVENT_TYPE_ASSOC,
+ EVENT_TYPE_AUTH,
+ __EVENT_TYPE_MAX,
+};
+
+enum usteer_node_type {
+ NODE_TYPE_LOCAL,
+ NODE_TYPE_REMOTE,
+};
+
+struct sta_info;
+struct usteer_local_node;
+
+struct usteer_node {
+ struct avl_node avl;
+ struct list_head sta_info;
+
+ enum usteer_node_type type;
+
+ struct blob_attr *rrm_nr;
+ struct blob_attr *script_data;
+ char ssid[33];
+
+ int freq;
+ int noise;
+ int n_assoc;
+ int max_assoc;
+ int load;
+};
+
+struct usteer_scan_request {
+ int n_freq;
+ int *freq;
+
+ bool passive;
+};
+
+struct usteer_scan_result {
+ uint8_t bssid[6];
+ char ssid[33];
+
+ int freq;
+ int signal;
+};
+
+struct usteer_survey_data {
+ uint16_t freq;
+ int8_t noise;
+
+ uint64_t time;
+ uint64_t time_busy;
+};
+
+struct usteer_freq_data {
+ uint16_t freq;
+
+ uint8_t txpower;
+ bool dfs;
+};
+
+struct usteer_node_handler {
+ struct list_head list;
+
+ void (*init_node)(struct usteer_node *);
+ void (*free_node)(struct usteer_node *);
+ void (*update_node)(struct usteer_node *);
+ void (*update_sta)(struct usteer_node *, struct sta_info *);
+ void (*get_survey)(struct usteer_node *, void *,
+ void (*cb)(void *priv, struct usteer_survey_data *d));
+ void (*get_freqlist)(struct usteer_node *, void *,
+ void (*cb)(void *priv, struct usteer_freq_data *f));
+ int (*scan)(struct usteer_node *, struct usteer_scan_request *,
+ void *, void (*cb)(void *priv, struct usteer_scan_result *r));
+};
+
+struct usteer_config {
+ bool syslog;
+ uint32_t debug_level;
+
+ uint32_t sta_block_timeout;
+ uint32_t local_sta_timeout;
+ uint32_t local_sta_update;
+
+ uint32_t max_retry_band;
+ uint32_t seen_policy_timeout;
+
+ uint32_t band_steering_threshold;
+ uint32_t load_balancing_threshold;
+
+ uint32_t remote_update_interval;
+ uint32_t remote_node_timeout;
+
+ int32_t min_snr;
+ int32_t min_connect_snr;
+ uint32_t signal_diff_threshold;
+
+ int32_t roam_scan_snr;
+ uint32_t roam_scan_tries;
+ uint32_t roam_scan_interval;
+
+ int32_t roam_trigger_snr;
+ uint32_t roam_trigger_interval;
+
+ uint32_t roam_kick_delay;
+
+ uint32_t initial_connect_delay;
+
+ bool load_kick_enabled;
+ uint32_t load_kick_threshold;
+ uint32_t load_kick_delay;
+ uint32_t load_kick_min_clients;
+ uint32_t load_kick_reason_code;
+
+ const char *node_up_script;
+};
+
+struct sta_info_stats {
+ uint32_t requests;
+ uint32_t blocked_cur;
+ uint32_t blocked_total;
+ uint32_t blocked_last_time;
+};
+
+#define __roam_trigger_states \
+ _S(IDLE) \
+ _S(SCAN) \
+ _S(SCAN_DONE) \
+ _S(WAIT_KICK) \
+ _S(NOTIFY_KICK) \
+ _S(KICK)
+
+enum roam_trigger_state {
+#define _S(n) ROAM_TRIGGER_##n,
+ __roam_trigger_states
+#undef _S
+};
+
+struct sta_info {
+ struct list_head list;
+ struct list_head node_list;
+
+ struct usteer_node *node;
+ struct sta *sta;
+
+ struct usteer_timeout timeout;
+
+ struct sta_info_stats stats[__EVENT_TYPE_MAX];
+ uint64_t created;
+ uint64_t seen;
+ int signal;
+
+ enum roam_trigger_state roam_state;
+ uint8_t roam_tries;
+ uint64_t roam_event;
+ uint64_t roam_kick;
+ uint64_t roam_scan_done;
+
+ int kick_count;
+
+ uint8_t scan_band : 1;
+ uint8_t connected : 2;
+};
+
+struct sta {
+ struct avl_node avl;
+ struct list_head nodes;
+
+ uint8_t seen_2ghz : 1;
+ uint8_t seen_5ghz : 1;
+
+ uint8_t addr[6];
+};
+
+extern struct ubus_context *ubus_ctx;
+extern struct usteer_config config;
+extern struct list_head node_handlers;
+extern struct avl_tree stations;
+extern uint64_t current_time;
+extern const char * const event_types[__EVENT_TYPE_MAX];
+
+void usteer_update_time(void);
+void usteer_init_defaults(void);
+bool usteer_handle_sta_event(struct usteer_node *node, const uint8_t *addr,
+ enum usteer_event_type type, int freq, int signal);
+
+void usteer_local_nodes_init(struct ubus_context *ctx);
+void usteer_local_node_kick(struct usteer_local_node *ln);
+
+void usteer_ubus_init(struct ubus_context *ctx);
+void usteer_ubus_kick_client(struct sta_info *si);
+int usteer_ubus_trigger_client_scan(struct sta_info *si);
+int usteer_ubus_notify_client_disassoc(struct sta_info *si);
+
+struct sta *usteer_sta_get(const uint8_t *addr, bool create);
+struct sta_info *usteer_sta_info_get(struct sta *sta, struct usteer_node *node, bool *create);
+
+void usteer_sta_info_update_timeout(struct sta_info *si, int timeout);
+void usteer_sta_info_update(struct sta_info *si, int signal, bool avg);
+
+static inline const char *usteer_node_name(struct usteer_node *node)
+{
+ return node->avl.key;
+}
+void usteer_node_set_blob(struct blob_attr **dest, struct blob_attr *val);
+
+bool usteer_check_request(struct sta_info *si, enum usteer_event_type type);
+
+void config_set_interfaces(struct blob_attr *data);
+void config_get_interfaces(struct blob_buf *buf);
+
+void config_set_node_up_script(struct blob_attr *data);
+void config_get_node_up_script(struct blob_buf *buf);
+
+int usteer_interface_init(void);
+void usteer_interface_add(const char *name);
+void usteer_sta_node_cleanup(struct usteer_node *node);
+void usteer_send_sta_update(struct sta_info *si);
+
+int usteer_lua_init(void);
+int usteer_lua_ubus_init(void);
+void usteer_run_hook(const char *name, const char *arg);
+
+#endif
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Copyright (C) 2020 embedd.ch
+ * Copyright (C) 2020 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2020 John Crispin <john@phrozen.org>
+ */
+
+#ifndef __APMGR_UTILS_H
+#define __APMGR_UTILS_H
+
+#define MSG(_nr, _format, ...) debug_msg(MSG_##_nr, __func__, __LINE__, _format, ##__VA_ARGS__)
+#define MSG_CONT(_nr, _format, ...) debug_msg_cont(MSG_##_nr, _format, ##__VA_ARGS__)
+
+#define MAC_ADDR_FMT "%02x:%02x:%02x:%02x:%02x:%02x"
+#define MAC_ADDR_DATA(_a) \
+ ((const uint8_t *)(_a))[0], \
+ ((const uint8_t *)(_a))[1], \
+ ((const uint8_t *)(_a))[2], \
+ ((const uint8_t *)(_a))[3], \
+ ((const uint8_t *)(_a))[4], \
+ ((const uint8_t *)(_a))[5]
+
+#define MSG_T_STA(_option, _sta_addr, _format, ...) \
+ MSG(DEBUG_ALL, "TESTCASE=" _option ",STA=" MAC_ADDR_FMT ": " _format, \
+ MAC_ADDR_DATA(_sta_addr), ##__VA_ARGS__)
+
+#define MSG_T(_option, _format, ...) \
+ MSG(DEBUG_ALL, "TESTCASE=" _option ": " _format, ##__VA_ARGS__)
+
+enum usteer_debug {
+ MSG_FATAL,
+ MSG_INFO,
+ MSG_VERBOSE,
+ MSG_DEBUG,
+ MSG_NETWORK,
+ MSG_DEBUG_ALL,
+};
+
+extern void debug_msg(int level, const char *func, int line, const char *format, ...);
+extern void debug_msg_cont(int level, const char *format, ...);
+
+#define __usteer_init __attribute__((constructor))
+
+#endif