diff --git a/hostapd/Makefile b/hostapd/Makefile index 2b33d57c2..65c5e9195 100644 --- a/hostapd/Makefile +++ b/hostapd/Makefile @@ -327,6 +327,11 @@ NEED_SHA384=y NEED_SHA512=y endif +ifdef CONFIG_AIRTIME_POLICY +CFLAGS += -DCONFIG_AIRTIME_POLICY +OBJS += ../src/ap/airtime_policy.o +endif + ifdef CONFIG_FILS CFLAGS += -DCONFIG_FILS OBJS += ../src/ap/fils_hlp.o diff --git a/hostapd/config_file.c b/hostapd/config_file.c index dce65570c..a3c58a5e3 100644 --- a/hostapd/config_file.c +++ b/hostapd/config_file.c @@ -2313,6 +2313,42 @@ static unsigned int parse_tls_flags(const char *val) #endif /* EAP_SERVER */ +#ifdef CONFIG_AIRTIME_POLICY +static int add_airtime_weight(struct hostapd_bss_config *bss, char *value) +{ + struct airtime_sta_weight *wt; + char *pos, *next; + + wt = os_zalloc(sizeof(*wt)); + if (!wt) + return -1; + + /* 02:01:02:03:04:05 10 */ + pos = value; + next = os_strchr(pos, ' '); + if (next) + *next++ = '\0'; + if (!next || hwaddr_aton(pos, wt->addr)) { + wpa_printf(MSG_ERROR, "Invalid station address: '%s'", pos); + os_free(wt); + return -1; + } + + pos = next; + wt->weight = atoi(pos); + if (!wt->weight) { + wpa_printf(MSG_ERROR, "Invalid weight: '%s'", pos); + os_free(wt); + return -1; + } + + wt->next = bss->airtime_weight_list; + bss->airtime_weight_list = wt; + return 0; +} +#endif /* CONFIG_AIRTIME_POLICY */ + + #ifdef CONFIG_SAE static int parse_sae_password(struct hostapd_bss_config *bss, const char *val) { @@ -4392,6 +4428,38 @@ static int hostapd_config_fill(struct hostapd_config *conf, conf->rssi_reject_assoc_timeout = atoi(pos); } else if (os_strcmp(buf, "pbss") == 0) { bss->pbss = atoi(pos); +#ifdef CONFIG_AIRTIME_POLICY + } else if (os_strcmp(buf, "airtime_mode") == 0) { + int val = atoi(pos); + + if (val < 0 || val > AIRTIME_MODE_MAX) { + wpa_printf(MSG_ERROR, "Line %d: Unknown airtime_mode", + line); + return 1; + } + conf->airtime_mode = val; + } else if (os_strcmp(buf, "airtime_update_interval") == 0) { + conf->airtime_update_interval = atoi(pos); + } else if (os_strcmp(buf, "airtime_bss_weight") == 0) { + bss->airtime_weight = atoi(pos); + } else if (os_strcmp(buf, "airtime_bss_limit") == 0) { + int val = atoi(pos); + + if (val < 0 || val > 1) { + wpa_printf(MSG_ERROR, + "Line %d: Invalid airtime_bss_limit (must be 0 or 1)", + line); + return 1; + } + bss->airtime_limit = val; + } else if (os_strcmp(buf, "airtime_sta_weight") == 0) { + if (add_airtime_weight(bss, pos) < 0) { + wpa_printf(MSG_ERROR, + "Line %d: Invalid airtime weight '%s'", + line, pos); + return 1; + } +#endif /* CONFIG_AIRTIME_POLICY */ } else { wpa_printf(MSG_ERROR, "Line %d: unknown configuration item '%s'", diff --git a/hostapd/defconfig b/hostapd/defconfig index ea5e2c9de..1f504ad18 100644 --- a/hostapd/defconfig +++ b/hostapd/defconfig @@ -376,6 +376,9 @@ CONFIG_IPV6=y # Experimental implementation of draft-harkins-owe-07.txt #CONFIG_OWE=y +# Airtime policy support +#CONFIG_AIRTIME_POLICY=y + # Override default value for the wpa_disable_eapol_key_retries configuration # parameter. See that parameter in hostapd.conf for more details. #CFLAGS += -DDEFAULT_WPA_DISABLE_EAPOL_KEY_RETRIES=1 diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf index 6dd4d0788..1702bd2e0 100644 --- a/hostapd/hostapd.conf +++ b/hostapd/hostapd.conf @@ -2496,6 +2496,42 @@ own_ip_addr=127.0.0.1 # that allows sending of such data. Default: 0. #stationary_ap=0 +##### Airtime policy configuration ########################################### + +# Set the airtime policy operating mode: +# 0 = disabled (default) +# 1 = static config +# 2 = per-BSS dynamic config +# 3 = per-BSS limit mode +#airtime_mode=0 + +# Interval (in milliseconds) to poll the kernel for updated station activity in +# dynamic and limit modes +#airtime_update_interval=200 + +# Static configuration of station weights (when airtime_mode=1). Kernel default +# weight is 256; set higher for larger airtime share, lower for smaller share. +# Each entry is a MAC address followed by a weight. +#airtime_sta_weight=02:01:02:03:04:05 256 +#airtime_sta_weight=02:01:02:03:04:06 512 + +# Per-BSS airtime weight. In multi-BSS mode, set for each BSS and hostapd will +# configure station weights to enforce the correct ratio between BSS weights +# depending on the number of active stations. The *ratios* between different +# BSSes is what's important, not the absolute numbers. +# Must be set for all BSSes if airtime_mode=2 or 3, has no effect otherwise. +#airtime_bss_weight=1 + +# Whether the current BSS should be limited (when airtime_mode=3). +# +# If set, the BSS weight ratio will be applied in the case where the current BSS +# would exceed the share defined by the BSS weight ratio. E.g., if two BSSes are +# set to the same weights, and one is set to limited, the limited BSS will get +# no more than half the available airtime, but if the non-limited BSS has more +# stations active, that *will* be allowed to exceed its half of the available +# airtime. +#airtime_bss_limit=1 + ##### TESTING OPTIONS ######################################################### # # The options in this section are only available when the build configuration diff --git a/src/ap/Makefile b/src/ap/Makefile index 9b07ee163..48f8f238d 100644 --- a/src/ap/Makefile +++ b/src/ap/Makefile @@ -20,6 +20,7 @@ CFLAGS += -DCONFIG_WPS CFLAGS += -DCONFIG_PROXYARP CFLAGS += -DCONFIG_IPV6 CFLAGS += -DCONFIG_IAPP +CFLAGS += -DCONFIG_AIRTIME_POLICY LIB_OBJS= \ accounting.o \ @@ -27,6 +28,7 @@ LIB_OBJS= \ ap_drv_ops.o \ ap_list.o \ ap_mlme.o \ + airtime_policy.o \ authsrv.o \ beacon.o \ bss_load.o \ diff --git a/src/ap/airtime_policy.c b/src/ap/airtime_policy.c new file mode 100644 index 000000000..f56ca5bdd --- /dev/null +++ b/src/ap/airtime_policy.c @@ -0,0 +1,269 @@ +/* + * Airtime policy configuration + * Copyright (c) 2018-2019, Toke Høiland-Jørgensen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#include "utils/includes.h" + +#include "utils/common.h" +#include "utils/eloop.h" +#include "hostapd.h" +#include "ap_drv_ops.h" +#include "sta_info.h" +#include "airtime_policy.h" + +/* Idea: + * Two modes of airtime enforcement: + * 1. Static weights: specify weights per MAC address with a per-BSS default + * 2. Per-BSS limits: Dynamically calculate weights of backlogged stations to + * enforce relative total shares between BSSes. + * + * - Periodic per-station callback to update queue status. + * + * Copy accounting_sta_update_stats() to get TXQ info and airtime weights and + * keep them updated in sta_info. + * + * - Separate periodic per-bss (or per-iface?) callback to update weights. + * + * Just need to loop through all interfaces, count sum the active stations (or + * should the per-STA callback just adjust that for the BSS?) and calculate new + * weights. + */ + +static int get_airtime_policy_update_timeout(struct hostapd_iface *iface, + unsigned int *sec, + unsigned int *usec) +{ + unsigned int update_int = iface->conf->airtime_update_interval; + + if (!update_int) { + wpa_printf(MSG_ERROR, + "Airtime policy: Invalid airtime policy update interval %u", + update_int); + return -1; + } + + *sec = update_int / 1000; + *usec = (update_int % 1000) * 1000; + + return 0; +} + + +static void set_new_backlog_time(struct hostapd_data *hapd, + struct sta_info *sta, + struct os_reltime *now) +{ + sta->backlogged_until = *now; + sta->backlogged_until.usec += hapd->iconf->airtime_update_interval * + AIRTIME_BACKLOG_EXPIRY_FACTOR; + while (sta->backlogged_until.usec >= 1000000) { + sta->backlogged_until.sec++; + sta->backlogged_until.usec -= 1000000; + } +} + + +static void count_backlogged_sta(struct hostapd_data *hapd) +{ + struct sta_info *sta; + struct hostap_sta_driver_data data = {}; + unsigned int num_backlogged = 0; + struct os_reltime now; + + os_get_reltime(&now); + + for (sta = hapd->sta_list; sta; sta = sta->next) { + if (hostapd_drv_read_sta_data(hapd, &data, sta->addr)) + continue; + + if (data.backlog_bytes > 0) + set_new_backlog_time(hapd, sta, &now); + if (os_reltime_before(&now, &sta->backlogged_until)) + num_backlogged++; + } + hapd->num_backlogged_sta = num_backlogged; +} + + +static int sta_set_airtime_weight(struct hostapd_data *hapd, + struct sta_info *sta, + unsigned int weight) +{ + int ret = 0; + + if (weight != sta->airtime_weight && + (ret = hostapd_sta_set_airtime_weight(hapd, sta->addr, weight))) + return ret; + + sta->airtime_weight = weight; + return ret; +} + + +static void set_sta_weights(struct hostapd_data *hapd, unsigned int weight) +{ + struct sta_info *sta; + + for (sta = hapd->sta_list; sta; sta = sta->next) + sta_set_airtime_weight(hapd, sta, weight); +} + + +static unsigned int get_airtime_quantum(unsigned int max_wt) +{ + unsigned int quantum = AIRTIME_QUANTUM_TARGET / max_wt; + + if (quantum < AIRTIME_QUANTUM_MIN) + quantum = AIRTIME_QUANTUM_MIN; + else if (quantum > AIRTIME_QUANTUM_MAX) + quantum = AIRTIME_QUANTUM_MAX; + + return quantum; +} + + +static void update_airtime_weights(void *eloop_data, void *user_data) +{ + struct hostapd_iface *iface = eloop_data; + struct hostapd_data *bss; + unsigned int sec, usec; + unsigned int num_sta_min = 0, num_sta_prod = 1, num_sta_sum = 0, + wt_sum = 0; + unsigned int quantum; + Boolean all_div_min = TRUE; + Boolean apply_limit = iface->conf->airtime_mode == AIRTIME_MODE_DYNAMIC; + int wt, num_bss = 0, max_wt = 0; + size_t i; + + for (i = 0; i < iface->num_bss; i++) { + bss = iface->bss[i]; + if (!bss->started || !bss->conf->airtime_weight) + continue; + + count_backlogged_sta(bss); + if (!bss->num_backlogged_sta) + continue; + + if (!num_sta_min || bss->num_backlogged_sta < num_sta_min) + num_sta_min = bss->num_backlogged_sta; + + num_sta_prod *= bss->num_backlogged_sta; + num_sta_sum += bss->num_backlogged_sta; + wt_sum += bss->conf->airtime_weight; + num_bss++; + } + + if (num_sta_min) { + for (i = 0; i < iface->num_bss; i++) { + bss = iface->bss[i]; + if (!bss->started || !bss->conf->airtime_weight) + continue; + + /* Check if we can divide all sta numbers by the + * smallest number to keep weights as small as possible. + * This is a lazy way to avoid having to factor + * integers. */ + if (bss->num_backlogged_sta && + bss->num_backlogged_sta % num_sta_min > 0) + all_div_min = FALSE; + + /* If we're in LIMIT mode, we only apply the weight + * scaling when the BSS(es) marked as limited would a + * larger share than the relative BSS weights indicates + * it should. */ + if (!apply_limit && bss->conf->airtime_limit) { + if (bss->num_backlogged_sta * wt_sum > + bss->conf->airtime_weight * num_sta_sum) + apply_limit = TRUE; + } + } + if (all_div_min) + num_sta_prod /= num_sta_min; + } + + for (i = 0; i < iface->num_bss; i++) { + bss = iface->bss[i]; + if (!bss->started || !bss->conf->airtime_weight) + continue; + + /* We only set the calculated weight if the BSS has active + * stations and there are other active interfaces as well - + * otherwise we just set a unit weight. This ensures that + * the weights are set reasonably when stations transition from + * inactive to active. */ + if (apply_limit && bss->num_backlogged_sta && num_bss > 1) + wt = bss->conf->airtime_weight * num_sta_prod / + bss->num_backlogged_sta; + else + wt = 1; + + bss->airtime_weight = wt; + if (wt > max_wt) + max_wt = wt; + } + + quantum = get_airtime_quantum(max_wt); + + for (i = 0; i < iface->num_bss; i++) { + bss = iface->bss[i]; + if (!bss->started || !bss->conf->airtime_weight) + continue; + set_sta_weights(bss, bss->airtime_weight * quantum); + } + + if (get_airtime_policy_update_timeout(iface, &sec, &usec) < 0) + return; + + eloop_register_timeout(sec, usec, update_airtime_weights, iface, + NULL); +} + + +static int get_weight_for_sta(struct hostapd_data *hapd, const u8 *sta) +{ + struct airtime_sta_weight *wt; + + wt = hapd->conf->airtime_weight_list; + while (wt && os_memcmp(wt->addr, sta, ETH_ALEN) != 0) + wt = wt->next; + + return wt ? wt->weight : hapd->conf->airtime_weight; +} + + +int airtime_policy_new_sta(struct hostapd_data *hapd, struct sta_info *sta) +{ + unsigned int weight; + + if (hapd->iconf->airtime_mode == AIRTIME_MODE_STATIC) { + weight = get_weight_for_sta(hapd, sta->addr); + if (weight) + return sta_set_airtime_weight(hapd, sta, weight); + } + return 0; +} + + +int airtime_policy_update_init(struct hostapd_iface *iface) +{ + unsigned int sec, usec; + + if (iface->conf->airtime_mode < AIRTIME_MODE_DYNAMIC) + return 0; + + if (get_airtime_policy_update_timeout(iface, &sec, &usec) < 0) + return -1; + + eloop_register_timeout(sec, usec, update_airtime_weights, iface, NULL); + return 0; +} + + +void airtime_policy_update_deinit(struct hostapd_iface *iface) +{ + eloop_cancel_timeout(update_airtime_weights, iface, NULL); +} diff --git a/src/ap/airtime_policy.h b/src/ap/airtime_policy.h new file mode 100644 index 000000000..c2a9b0080 --- /dev/null +++ b/src/ap/airtime_policy.h @@ -0,0 +1,48 @@ +/* + * Airtime policy configuration + * Copyright (c) 2018-2019, Toke Høiland-Jørgensen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#ifndef AIRTIME_POLICY_H +#define AIRTIME_POLICY_H + +struct hostapd_iface; + +#ifdef CONFIG_AIRTIME_POLICY + +#define AIRTIME_DEFAULT_UPDATE_INTERVAL 200 /* ms */ +#define AIRTIME_BACKLOG_EXPIRY_FACTOR 2500 /* 2.5 intervals + convert to usec */ + +/* scale quantum so this becomes the effective quantum after applying the max + * weight, but never go below min or above max */ +#define AIRTIME_QUANTUM_MIN 8 /* usec */ +#define AIRTIME_QUANTUM_MAX 256 /* usec */ +#define AIRTIME_QUANTUM_TARGET 1024 /* usec */ + +int airtime_policy_new_sta(struct hostapd_data *hapd, struct sta_info *sta); +int airtime_policy_update_init(struct hostapd_iface *iface); +void airtime_policy_update_deinit(struct hostapd_iface *iface); + +#else /* CONFIG_AIRTIME_POLICY */ + +static inline int airtime_policy_new_sta(struct hostapd_data *hapd, + struct sta_info *sta) +{ + return -1; +} + +static inline int airtime_policy_update_init(struct hostapd_iface *iface) +{ + return -1; +} + +static inline void airtime_policy_update_deinit(struct hostapd_iface *iface) +{ +} + +#endif /* CONFIG_AIRTIME_POLICY */ + +#endif /* AIRTIME_POLICY_H */ diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c index 2ace6023a..09ab3727a 100644 --- a/src/ap/ap_config.c +++ b/src/ap/ap_config.c @@ -19,6 +19,7 @@ #include "eap_server/eap.h" #include "wpa_auth.h" #include "sta_info.h" +#include "airtime_policy.h" #include "ap_config.h" @@ -249,6 +250,10 @@ struct hostapd_config * hostapd_config_defaults(void) conf->rssi_reject_assoc_rssi = 0; conf->rssi_reject_assoc_timeout = 30; +#ifdef CONFIG_AIRTIME_POLICY + conf->airtime_update_interval = AIRTIME_DEFAULT_UPDATE_INTERVAL; +#endif /* CONFIG_AIRTIME_POLICY */ + return conf; } @@ -766,6 +771,20 @@ void hostapd_config_free_bss(struct hostapd_bss_config *conf) hostapd_config_free_sae_passwords(conf); +#ifdef CONFIG_AIRTIME_POLICY + { + struct airtime_sta_weight *wt, *wt_prev; + + wt = conf->airtime_weight_list; + conf->airtime_weight_list = NULL; + while (wt) { + wt_prev = wt; + wt = wt->next; + os_free(wt_prev); + } + } +#endif /* CONFIG_AIRTIME_POLICY */ + os_free(conf); } @@ -1162,6 +1181,13 @@ int hostapd_config_check(struct hostapd_config *conf, int full_config) return -1; } +#ifdef CONFIG_AIRTIME_POLICY + if (full_config && conf->airtime_mode > AIRTIME_MODE_STATIC && + !conf->airtime_update_interval) { + wpa_printf(MSG_ERROR, "Airtime update interval cannot be zero"); + return -1; + } +#endif /* CONFIG_AIRTIME_POLICY */ for (i = 0; i < NUM_TX_QUEUES; i++) { if (hostapd_config_check_cw(conf, i)) return -1; diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h index f18425224..b0b5c6f3f 100644 --- a/src/ap/ap_config.h +++ b/src/ap/ap_config.h @@ -259,6 +259,12 @@ struct dpp_controller_conf { struct hostapd_ip_addr ipaddr; }; +struct airtime_sta_weight { + struct airtime_sta_weight *next; + unsigned int weight; + u8 addr[ETH_ALEN]; +}; + /** * struct hostapd_bss_config - Per-BSS configuration */ @@ -719,6 +725,12 @@ struct hostapd_bss_config { #define BACKHAUL_BSS 1 #define FRONTHAUL_BSS 2 int multi_ap; /* bitmap of BACKHAUL_BSS, FRONTHAUL_BSS */ + +#ifdef CONFIG_AIRTIME_POLICY + unsigned int airtime_weight; + int airtime_limit; + struct airtime_sta_weight *airtime_weight_list; +#endif /* CONFIG_AIRTIME_POLICY */ }; /** @@ -884,6 +896,18 @@ struct hostapd_config { int rssi_reject_assoc_rssi; int rssi_reject_assoc_timeout; + +#ifdef CONFIG_AIRTIME_POLICY + enum { + AIRTIME_MODE_OFF = 0, + AIRTIME_MODE_STATIC = 1, + AIRTIME_MODE_DYNAMIC = 2, + AIRTIME_MODE_LIMIT = 3, + __AIRTIME_MODE_MAX, + } airtime_mode; + unsigned int airtime_update_interval; +#define AIRTIME_MODE_MAX (__AIRTIME_MODE_MAX - 1) +#endif /* CONFIG_AIRTIME_POLICY */ }; diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c index 0bd689238..d0fecc895 100644 --- a/src/ap/hostapd.c +++ b/src/ap/hostapd.c @@ -50,6 +50,7 @@ #include "fils_hlp.h" #include "acs.h" #include "hs20.h" +#include "airtime_policy.h" static int hostapd_flush_old_stations(struct hostapd_data *hapd, u16 reason); @@ -491,6 +492,7 @@ static void hostapd_cleanup_iface_partial(struct hostapd_iface *iface) iface->basic_rates = NULL; ap_list_deinit(iface); sta_track_deinit(iface); + airtime_policy_update_deinit(iface); } @@ -1976,6 +1978,7 @@ dfs_offload: hostapd_set_state(iface, HAPD_IFACE_ENABLED); hostapd_owe_update_trans(iface); + airtime_policy_update_init(iface); wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AP_EVENT_ENABLED); if (hapd->setup_complete_cb) hapd->setup_complete_cb(hapd->setup_complete_cb_ctx); @@ -2996,6 +2999,8 @@ void hostapd_new_assoc_sta(struct hostapd_data *hapd, struct sta_info *sta, } #endif /* CONFIG_P2P */ + airtime_policy_new_sta(hapd, sta); + /* Start accounting here, if IEEE 802.1X and WPA are not used. * IEEE 802.1X/WPA code will start accounting after the station has * been authorized. */ diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h index 0f76cd6c8..594699f3c 100644 --- a/src/ap/hostapd.h +++ b/src/ap/hostapd.h @@ -379,6 +379,11 @@ struct hostapd_data { unsigned int dpp_ignore_netaccesskey_mismatch:1; #endif /* CONFIG_TESTING_OPTIONS */ #endif /* CONFIG_DPP */ + +#ifdef CONFIG_AIRTIME_POLICY + unsigned int num_backlogged_sta; + unsigned int airtime_weight; +#endif /* CONFIG_AIRTIME_POLICY */ }; @@ -541,6 +546,9 @@ struct hostapd_iface { unsigned int num_sta_seen; u8 dfs_domain; +#ifdef CONFIG_AIRTIME_POLICY + unsigned int airtime_quantum; +#endif /* CONFIG_AIRTIME_POLICY */ }; /* hostapd.c */ diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h index ece0c60ab..c185d7a0a 100644 --- a/src/ap/sta_info.h +++ b/src/ap/sta_info.h @@ -275,6 +275,10 @@ struct sta_info { u8 last_tk[WPA_TK_MAX_LEN]; size_t last_tk_len; #endif /* CONFIG_TESTING_OPTIONS */ +#ifdef CONFIG_AIRTIME_POLICY + unsigned int airtime_weight; + struct os_reltime backlogged_until; +#endif /* CONFIG_AIRTIME_POLICY */ };