From: Felix Fietkau Date: Wed, 21 Mar 2018 15:40:05 +0000 (+0100) Subject: usteer: Initial import X-Git-Url: http://git.openwrt.org/?a=commitdiff_plain;h=d10731f8060d3937ef562858f5a52ca7f910da31;p=project%2Fusteer.git usteer: Initial import Signed-off-by: Felix Fietkau --- d10731f8060d3937ef562858f5a52ca7f910da31 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