From d10731f8060d3937ef562858f5a52ca7f910da31 Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Wed, 21 Mar 2018 16:40:05 +0100 Subject: [PATCH] usteer: Initial import Signed-off-by: Felix Fietkau --- .gitignore | 8 + CMakeLists.txt | 51 +++ fakeap.c | 244 +++++++++++ local_node.c | 511 ++++++++++++++++++++++ main.c | 163 +++++++ monitor.c | 207 +++++++++ netifd.c | 154 +++++++ nl80211.c | 510 ++++++++++++++++++++++ node.c | 38 ++ node.h | 79 ++++ openwrt/usteer/Makefile | 37 ++ openwrt/usteer/files/etc/config/usteer | 4 + openwrt/usteer/files/etc/init.d/usteer | 120 ++++++ parse.c | 142 +++++++ policy.c | 436 +++++++++++++++++++ remote.c | 562 +++++++++++++++++++++++++ remote.h | 87 ++++ sta.c | 210 +++++++++ timeout.c | 160 +++++++ timeout.h | 49 +++ ubus.c | 419 ++++++++++++++++++ usteer.h | 265 ++++++++++++ utils.h | 56 +++ 23 files changed, 4512 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 fakeap.c create mode 100644 local_node.c create mode 100644 main.c create mode 100644 monitor.c create mode 100644 netifd.c create mode 100644 nl80211.c create mode 100644 node.c create mode 100644 node.h create mode 100644 openwrt/usteer/Makefile create mode 100644 openwrt/usteer/files/etc/config/usteer create mode 100755 openwrt/usteer/files/etc/init.d/usteer create mode 100644 parse.c create mode 100644 policy.c create mode 100644 remote.c create mode 100644 remote.h create mode 100644 sta.c create mode 100644 timeout.c create mode 100644 timeout.h create mode 100644 ubus.c create mode 100644 usteer.h create mode 100644 utils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..717a7a2 --- /dev/null +++ b/.gitignore @@ -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 index 0000000..59aa5cf --- /dev/null +++ b/CMakeLists.txt @@ -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 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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include +#include +#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 \n" + "Options:\n" + " -p [-]: probing interval (fixed or min-max)\n" + " -s [-]: rssi (signal strength) (fixed or min-max)\n" + " -n : create stations\n" + " -f : 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 index 0000000..07af00e --- /dev/null +++ b/local_node.c @@ -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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include +#ifdef linux +#include +#endif +#include +#include + +#include +#include +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include + +#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 : Connect to other instances on interface \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 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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include + +#include + +#include + +#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 \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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#define _GNU_SOURCE +#include +#include + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 index 0000000..cf8e2cb --- /dev/null +++ b/openwrt/usteer/Makefile @@ -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 index 0000000..9be3d85 --- /dev/null +++ b/openwrt/usteer/files/etc/config/usteer @@ -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 index 0000000..d73c622 --- /dev/null +++ b/openwrt/usteer/files/etc/init.d/usteer @@ -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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#ifndef __APMGR_REMOTE_H +#define __APMGR_REMOTE_H + +#include + +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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#include + +#include + +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#ifndef __APMGR_TIMEOUT_H +#define __APMGR_TIMEOUT_H + +#include +#include + +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 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 + * Copyright (C) 2020 John Crispin + */ + +#include +#include +#include +#ifdef linux +#include +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#ifndef __APMGR_H +#define __APMGR_H + +#include +#include +#include +#include +#include +#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 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 + * Copyright (C) 2020 John Crispin + */ + +#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 -- 2.30.2