usteer: Initial import
authorFelix Fietkau <nbd@nbd.name>
Wed, 21 Mar 2018 15:40:05 +0000 (16:40 +0100)
committerJohn Crispin <john@phrozen.org>
Thu, 12 Nov 2020 14:37:10 +0000 (15:37 +0100)
Signed-off-by: Felix Fietkau <nbd@nbd.name>
23 files changed:
.gitignore [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
fakeap.c [new file with mode: 0644]
local_node.c [new file with mode: 0644]
main.c [new file with mode: 0644]
monitor.c [new file with mode: 0644]
netifd.c [new file with mode: 0644]
nl80211.c [new file with mode: 0644]
node.c [new file with mode: 0644]
node.h [new file with mode: 0644]
openwrt/usteer/Makefile [new file with mode: 0644]
openwrt/usteer/files/etc/config/usteer [new file with mode: 0644]
openwrt/usteer/files/etc/init.d/usteer [new file with mode: 0755]
parse.c [new file with mode: 0644]
policy.c [new file with mode: 0644]
remote.c [new file with mode: 0644]
remote.h [new file with mode: 0644]
sta.c [new file with mode: 0644]
timeout.c [new file with mode: 0644]
timeout.h [new file with mode: 0644]
ubus.c [new file with mode: 0644]
usteer.h [new file with mode: 0644]
utils.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..717a7a2
--- /dev/null
@@ -0,0 +1,8 @@
+/Makefile
+CMakeCache.txt
+CMakeFiles
+*.cmake
+install_manifest.txt
+/usteerd
+/ap-monitor
+/fakeap
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..59aa5cf
--- /dev/null
@@ -0,0 +1,51 @@
+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
+)
diff --git a/fakeap.c b/fakeap.c
new file mode 100644 (file)
index 0000000..8b66bf1
--- /dev/null
+++ b/fakeap.c
@@ -0,0 +1,244 @@
+/*
+ *   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]);
+}
diff --git a/local_node.c b/local_node.c
new file mode 100644 (file)
index 0000000..07af00e
--- /dev/null
@@ -0,0 +1,511 @@
+/*
+ *   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);
+}
diff --git a/main.c b/main.c
new file mode 100644 (file)
index 0000000..b6dbffa
--- /dev/null
+++ b/main.c
@@ -0,0 +1,163 @@
+/*
+ *   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;
+}
diff --git a/monitor.c b/monitor.c
new file mode 100644 (file)
index 0000000..273b412
--- /dev/null
+++ b/monitor.c
@@ -0,0 +1,207 @@
+/*
+ *   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;
+}
diff --git a/netifd.c b/netifd.c
new file mode 100644 (file)
index 0000000..5836097
--- /dev/null
+++ b/netifd.c
@@ -0,0 +1,154 @@
+/*
+ *   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);
+}
diff --git a/nl80211.c b/nl80211.c
new file mode 100644 (file)
index 0000000..fc3add9
--- /dev/null
+++ b/nl80211.c
@@ -0,0 +1,510 @@
+/*
+ *   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);
+}
diff --git a/node.c b/node.c
new file mode 100644 (file)
index 0000000..2858dac
--- /dev/null
+++ b/node.c
@@ -0,0 +1,38 @@
+/*
+ *   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);
+}
diff --git a/node.h b/node.h
new file mode 100644 (file)
index 0000000..efe9e1c
--- /dev/null
+++ b/node.h
@@ -0,0 +1,79 @@
+/*
+ *   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
diff --git a/openwrt/usteer/Makefile b/openwrt/usteer/Makefile
new file mode 100644 (file)
index 0000000..cf8e2cb
--- /dev/null
@@ -0,0 +1,37 @@
+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))
diff --git a/openwrt/usteer/files/etc/config/usteer b/openwrt/usteer/files/etc/config/usteer
new file mode 100644 (file)
index 0000000..9be3d85
--- /dev/null
@@ -0,0 +1,4 @@
+config usteer
+       option 'network' 'lan'
+       option 'syslog' '1'
+       option 'debug_level' '2'
diff --git a/openwrt/usteer/files/etc/init.d/usteer b/openwrt/usteer/files/etc/init.d/usteer
new file mode 100755 (executable)
index 0000000..d73c622
--- /dev/null
@@ -0,0 +1,120 @@
+#!/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
+}
diff --git a/parse.c b/parse.c
new file mode 100644 (file)
index 0000000..800ae5a
--- /dev/null
+++ b/parse.c
@@ -0,0 +1,142 @@
+/*
+ *   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;
+}
diff --git a/policy.c b/policy.c
new file mode 100644 (file)
index 0000000..6f75f69
--- /dev/null
+++ b/policy.c
@@ -0,0 +1,436 @@
+/*
+ *   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);
+}
diff --git a/remote.c b/remote.c
new file mode 100644 (file)
index 0000000..3fad95f
--- /dev/null
+++ b/remote.c
@@ -0,0 +1,562 @@
+/*
+ *   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;
+}
diff --git a/remote.h b/remote.h
new file mode 100644 (file)
index 0000000..6cc5e30
--- /dev/null
+++ b/remote.h
@@ -0,0 +1,87 @@
+/*
+ *   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
diff --git a/sta.c b/sta.c
new file mode 100644 (file)
index 0000000..d9fcb60
--- /dev/null
+++ b/sta.c
@@ -0,0 +1,210 @@
+/*
+ *   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;
+}
diff --git a/timeout.c b/timeout.c
new file mode 100644 (file)
index 0000000..d1986f1
--- /dev/null
+++ b/timeout.c
@@ -0,0 +1,160 @@
+/*
+ *   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);
+       }
+}
diff --git a/timeout.h b/timeout.h
new file mode 100644 (file)
index 0000000..be48bc7
--- /dev/null
+++ b/timeout.h
@@ -0,0 +1,49 @@
+/*
+ *   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
diff --git a/ubus.c b/ubus.c
new file mode 100644 (file)
index 0000000..2aa82e5
--- /dev/null
+++ b/ubus.c
@@ -0,0 +1,419 @@
+/*
+ *   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);
+}
diff --git a/usteer.h b/usteer.h
new file mode 100644 (file)
index 0000000..0041886
--- /dev/null
+++ b/usteer.h
@@ -0,0 +1,265 @@
+/*
+ *   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
diff --git a/utils.h b/utils.h
new file mode 100644 (file)
index 0000000..9b34add
--- /dev/null
+++ b/utils.h
@@ -0,0 +1,56 @@
+/*
+ *   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