[RFC] hostapd: Add airtime policy configuration support
diff mbox series

Message ID 20190215181129.21216-1-toke@toke.dk
State New
Headers show
Series
  • [RFC] hostapd: Add airtime policy configuration support
Related show

Commit Message

Toke Høiland-Jørgensen Feb. 15, 2019, 6:11 p.m. UTC
This adds support to hostapd for configuring airtime policy settings for
stations as they connect to the access point. This is the userspace
component of the airtime policy enforcement system PoliFi described in this
paper: https://arxiv.org/abs/1902.03439

The kernel part has been merged into mac80211 for the 5.1 dev cycle.

The configuration mechanism has three modes: Static, dynamic and limit. In
static mode, weights can be set in the configuration file for individual
MAC addresses, which will be applied when the configured stations connect.

In dynamic mode, weights are instead set per BSS, which will be scaled by
the number of active stations on that BSS, achieving the desired aggregate
weighing between the configured BSSes. Limit mode works like dynamic mode,
except that any BSS *not* marked as 'limited' is allowed to exceed its
configured share if a per-station fairness share would assign more airtime
to that BSS. See the paper for details on these modes.

Signed-off-by: Toke Høiland-Jørgensen <toke@toke.dk>
---
 hostapd/Makefile             |   6 +
 hostapd/config_file.c        |  52 ++++++++
 hostapd/defconfig            |   3 +
 hostapd/hostapd.conf         |  36 +++++
 src/ap/Makefile              |   2 +
 src/ap/airtime_policy.c      | 252 +++++++++++++++++++++++++++++++++++
 src/ap/airtime_policy.h      |  31 +++++
 src/ap/ap_config.c           |  42 ++++++
 src/ap/ap_config.h           |  24 ++++
 src/ap/ap_drv_ops.c          |   8 ++
 src/ap/ap_drv_ops.h          |   2 +
 src/ap/hostapd.c             |  10 ++
 src/ap/hostapd.h             |   8 ++
 src/ap/sta_info.h            |   4 +
 src/drivers/driver.h         |  13 ++
 src/drivers/driver_nl80211.c |  56 ++++++++
 16 files changed, 549 insertions(+)
 create mode 100644 src/ap/airtime_policy.c
 create mode 100644 src/ap/airtime_policy.h

Comments

Dave Taht Feb. 15, 2019, 7:08 p.m. UTC | #1
To me, this does not need to be a config option but be enabled by
default. Secondly, I'm not sure if it will even compile if disabled,
as .sta_set_airtime_weight is not within the config option.
Toke Høiland-Jørgensen Feb. 15, 2019, 11:05 p.m. UTC | #2
Dave Taht <dave.taht@gmail.com> writes:

> To me, this does not need to be a config option but be enabled by
> default. Secondly, I'm not sure if it will even compile if disabled,
> as .sta_set_airtime_weight is not within the config option.

The config is all for setting policy; if you don't configure anything
you'll just get per-station fairness...

-Toke

Patch
diff mbox series

diff --git a/hostapd/Makefile b/hostapd/Makefile
index dd3816e0d..325372801 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -326,6 +326,12 @@  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 aeec1d9e2..cefa90e85 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2296,6 +2296,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 == NULL)
+		return -1;
+
+	/* 02:01:02:03:04:05 10 */
+	pos = value;
+	next = os_strchr(pos, ' ');
+	if (next)
+		*next++ = '\0';
+	if (next == NULL || 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)
@@ -4262,6 +4298,22 @@  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) {
+		conf->airtime_mode = atoi(pos);
+	} 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) {
+		bss->airtime_limit = atoi(pos);
+	} else if (os_strcmp(buf, "airtime_sta_weight") == 0) {
+		if (add_airtime_weight(bss, pos) < 0) {
+			wpa_printf(MSG_DEBUG, "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..10c032e82 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 supprot
+#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 ab37f03b5..ea2471862 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -2422,6 +2422,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..7cfd9a48b 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_AIRITME_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..4ceb0f51c
--- /dev/null
+++ b/src/ap/airtime_policy.c
@@ -0,0 +1,252 @@ 
+/*
+ * Airtime policy configuration
+ * Copyright (c) 2018-2019, Toke Høiland-Jørgensen <toke@toke.dk>
+ *
+ * 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 "airtime_policy.h"
+#include "ap_drv_ops.h"
+#include "sta_info.h"
+
+#ifdef CONFIG_AIRTIME_POLICY
+
+/* 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 inline 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, apply_limit = (iface->conf->airtime_mode == AIRTIME_MODE_DYNAMIC);
+	int i, wt, num_bss = 0, max_wt = 0;
+
+	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 != NULL && os_memcmp(wt->addr, sta, 6) != 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)
+{
+	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);
+}
+
+#endif /* CONFIG_AIRTIME_POLICY */
diff --git a/src/ap/airtime_policy.h b/src/ap/airtime_policy.h
new file mode 100644
index 000000000..483d10928
--- /dev/null
+++ b/src/ap/airtime_policy.h
@@ -0,0 +1,31 @@ 
+/*
+ * Airtime policy configuration
+ * Copyright (c) 2018-2019, Toke Høiland-Jørgensen <toke@toke.dk>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef AIRTIME_POLICY_LOAD_H
+#define AIRTIME_POLICY_LOAD_H
+
+#include "hostapd.h"
+
+#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);
+
+#endif /* CONFIG_AIRTIME_POLICY */
+
+#endif /* AIRTIME_POLICY_LOAD_H */
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 9611dc0b3..4b004b3d7 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"
 
 
@@ -244,6 +245,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
+
 	return conf;
 }
 
@@ -734,6 +739,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);
 }
 
@@ -1068,6 +1087,18 @@  static int hostapd_config_check_bss(struct hostapd_bss_config *bss,
 	}
 #endif /* CONFIG_OCV */
 
+#ifdef CONFIG_AIRTIME_POLICY
+	if (full_config && bss->airtime_weight < 0) {
+		wpa_printf(MSG_ERROR, "Airtime BSS weight must be >= 0");
+		return -1;
+	}
+
+	if (full_config && (bss->airtime_limit < 0 || bss->airtime_limit > 1)) {
+		wpa_printf(MSG_ERROR, "Airtime BSS limit must be 0 or 1");
+		return -1;
+	}
+#endif /* CONFIG_AIRTIME_POLICY */
+
 	return 0;
 }
 
@@ -1124,6 +1155,17 @@  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_MAX) {
+		wpa_printf(MSG_ERROR, "Unknown airtime mode");
+		return -1;
+	}
+
+	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 6963df460..1ed53c9a0 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -250,6 +250,12 @@  struct sae_password_entry {
 	u8 peer_addr[ETH_ALEN];
 };
 
+struct airtime_sta_weight {
+	struct airtime_sta_weight *next;
+	int weight;
+	u8 addr[6];
+};
+
 /**
  * struct hostapd_bss_config - Per-BSS configuration
  */
@@ -704,6 +710,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
+	int airtime_weight;
+	int airtime_limit;
+	struct airtime_sta_weight *airtime_weight_list;
+#endif /* CONFIG_AIRTIME_POLICY */
 };
 
 /**
@@ -856,6 +868,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/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index 067cf863e..cfc55952a 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -582,6 +582,14 @@  int hostapd_sta_set_flags(struct hostapd_data *hapd, u8 *addr,
 					   flags_or, flags_and);
 }
 
+int hostapd_sta_set_airtime_weight(struct hostapd_data *hapd, u8 *addr,
+				   unsigned int weight)
+{
+	if (hapd->driver == NULL || hapd->driver->sta_set_airtime_weight == NULL)
+		return 0;
+	return hapd->driver->sta_set_airtime_weight(hapd->drv_priv, addr, weight);
+}
+
 
 int hostapd_set_country(struct hostapd_data *hapd, const char *country)
 {
diff --git a/src/ap/ap_drv_ops.h b/src/ap/ap_drv_ops.h
index d45ab8462..a40855d96 100644
--- a/src/ap/ap_drv_ops.h
+++ b/src/ap/ap_drv_ops.h
@@ -67,6 +67,8 @@  int hostapd_set_rts(struct hostapd_data *hapd, int rts);
 int hostapd_set_frag(struct hostapd_data *hapd, int frag);
 int hostapd_sta_set_flags(struct hostapd_data *hapd, u8 *addr,
 			  int total_flags, int flags_or, int flags_and);
+int hostapd_sta_set_airtime_weight(struct hostapd_data *hapd, u8 *addr,
+				   unsigned int weight);
 int hostapd_set_country(struct hostapd_data *hapd, const char *country);
 int hostapd_set_tx_queue_params(struct hostapd_data *hapd, int queue, int aifs,
 				int cw_min, int cw_max, int burst_time);
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index 342585f92..b8db488da 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);
@@ -476,6 +477,9 @@  static void hostapd_cleanup_iface_partial(struct hostapd_iface *iface)
 	iface->basic_rates = NULL;
 	ap_list_deinit(iface);
 	sta_track_deinit(iface);
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_update_deinit(iface);
+#endif
 }
 
 
@@ -1954,6 +1958,9 @@  dfs_offload:
 
 	hostapd_set_state(iface, HAPD_IFACE_ENABLED);
 	hostapd_owe_update_trans(iface);
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_update_init(iface);
+#endif
 	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);
@@ -2966,6 +2973,9 @@  void hostapd_new_assoc_sta(struct hostapd_data *hapd, struct sta_info *sta,
 			hostapd_p2p_non_p2p_sta_connected(hapd);
 	}
 #endif /* CONFIG_P2P */
+#ifdef CONFIG_AIRTIME_POLICY
+	airtime_policy_new_sta(hapd, sta);
+#endif /* CONFIG_AIRTIME_POLICY */
 
 	/* Start accounting here, if IEEE 802.1X and WPA are not used.
 	 * IEEE 802.1X/WPA code will start accounting after the station has
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index d304c1171..c92983c51 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -371,6 +371,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 */
 };
 
 
@@ -533,6 +538,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 ee3f628a4..1e7706811 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -271,6 +271,10 @@  struct sta_info {
 	u8 last_tk[WPA_TK_MAX_LEN];
 	size_t last_tk_len;
 #endif /* CONFIG_TESTING_OPTIONS */
+#ifdef CONFIG_AIRTIME_POLICY
+	int airtime_weight;
+	struct os_reltime backlogged_until;
+#endif
 };
 
 
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index 23423d92e..eda22f89b 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -1752,6 +1752,7 @@  struct hostapd_data;
 struct hostap_sta_driver_data {
 	unsigned long rx_packets, tx_packets;
 	unsigned long long rx_bytes, tx_bytes;
+	unsigned long long rx_airtime, tx_airtime;
 	int bytes_64bit; /* whether 64-bit byte counters are supported */
 	unsigned long current_tx_rate;
 	unsigned long current_rx_rate;
@@ -1761,6 +1762,8 @@  struct hostap_sta_driver_data {
 	unsigned long tx_retry_failed;
 	unsigned long tx_retry_count;
 	s8 last_ack_rssi;
+	unsigned long backlog_packets;
+	unsigned long backlog_bytes;
 	s8 signal;
 	u8 rx_vhtmcs;
 	u8 tx_vhtmcs;
@@ -2935,6 +2938,16 @@  struct wpa_driver_ops {
 			     unsigned int total_flags, unsigned int flags_or,
 			     unsigned int flags_and);
 
+	/**
+	 * sta_set_airtime_weight - Set station airtime weight (AP only)
+	 * @priv: Private driver interface data
+	 * @addr: Station address
+	 * @weight: New weight for station airtime assignment
+	 * Returns: 0 on success, -1 on failure
+	 */
+	int (*sta_set_airtime_weight)(void *priv, const u8 *addr,
+				unsigned int weight);
+
 	/**
 	 * set_tx_queue_params - Set TX queue parameters
 	 * @priv: Private driver interface data
diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c
index e0e6fe52c..f4ab3b6cd 100644
--- a/src/drivers/driver_nl80211.c
+++ b/src/drivers/driver_nl80211.c
@@ -5169,6 +5169,27 @@  fail:
 	return -ENOBUFS;
 }
 
+static int driver_nl80211_sta_set_airtime_weight(void *priv, const u8 *addr,
+						 unsigned int weight)
+{
+	struct i802_bss *bss = priv;
+	struct nl_msg *msg;
+	struct nlattr *flags;
+
+	wpa_printf(MSG_DEBUG, "nl80211: Set STA airtime weight - ifname=%s addr=" MACSTR
+		   " weight=%u", bss->ifname, MAC2STR(addr), weight);
+
+	if (!(msg = nl80211_bss_msg(bss, 0, NL80211_CMD_SET_STATION)) ||
+	    nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, addr) ||
+	    nla_put_u16(msg, NL80211_ATTR_AIRTIME_WEIGHT, weight))
+		goto fail;
+
+	return send_and_recv_msgs(bss->drv, msg, NULL, NULL);
+fail:
+	nlmsg_free(msg);
+	return -ENOBUFS;
+}
+
 
 static int wpa_driver_nl80211_ap(struct wpa_driver_nl80211_data *drv,
 				 struct wpa_driver_associate_params *params)
@@ -6253,6 +6274,8 @@  static int get_sta_handler(struct nl_msg *msg, void *arg)
 		[NL80211_STA_INFO_TX_BYTES64] = { .type = NLA_U64 },
 		[NL80211_STA_INFO_SIGNAL] = { .type = NLA_U8 },
 		[NL80211_STA_INFO_ACK_SIGNAL] = { .type = NLA_U8 },
+		[NL80211_STA_INFO_RX_DURATION] = { .type = NLA_U64 },
+		[NL80211_STA_INFO_TX_DURATION] = { .type = NLA_U64 },
 	};
 	struct nlattr *rate[NL80211_RATE_INFO_MAX + 1];
 	static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
@@ -6263,6 +6286,13 @@  static int get_sta_handler(struct nl_msg *msg, void *arg)
 		[NL80211_RATE_INFO_SHORT_GI] = { .type = NLA_FLAG },
 		[NL80211_RATE_INFO_VHT_NSS] = { .type = NLA_U8 },
 	};
+	struct nlattr *tid_stats[NL80211_TID_STATS_MAX + 1], *tidattr;
+	struct nlattr *txq_stats[NL80211_TXQ_STATS_MAX + 1];
+	static struct nla_policy txq_stats_policy[NL80211_TXQ_STATS_MAX +1] = {
+		[NL80211_TXQ_STATS_BACKLOG_BYTES] = { .type = NLA_U32 },
+		[NL80211_TXQ_STATS_BACKLOG_PACKETS] = { .type = NLA_U32 },
+	};
+	int rem;
 
 	nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
 		  genlmsg_attrlen(gnlh, 0), NULL);
@@ -6310,6 +6340,10 @@  static int get_sta_handler(struct nl_msg *msg, void *arg)
 	if (stats[NL80211_STA_INFO_TX_PACKETS])
 		data->tx_packets =
 			nla_get_u32(stats[NL80211_STA_INFO_TX_PACKETS]);
+	if (stats[NL80211_STA_INFO_RX_DURATION])
+		data->rx_airtime = nla_get_u64(stats[NL80211_STA_INFO_RX_DURATION]);
+	if (stats[NL80211_STA_INFO_TX_DURATION])
+		data->tx_airtime = nla_get_u64(stats[NL80211_STA_INFO_TX_DURATION]);
 	if (stats[NL80211_STA_INFO_TX_FAILED])
 		data->tx_retry_failed =
 			nla_get_u32(stats[NL80211_STA_INFO_TX_FAILED]);
@@ -6380,6 +6414,27 @@  static int get_sta_handler(struct nl_msg *msg, void *arg)
 		}
 	}
 
+	if (stats[NL80211_STA_INFO_TID_STATS]) {
+		nla_for_each_nested(tidattr, stats[NL80211_STA_INFO_TID_STATS], rem) {
+			if (nla_parse_nested(tid_stats, NL80211_TID_STATS_MAX,
+					     tidattr, NULL) == 0 &&
+			    tid_stats[NL80211_TID_STATS_TXQ_STATS] &&
+			    nla_parse_nested(txq_stats, NL80211_TXQ_STATS_MAX,
+					     tid_stats[NL80211_TID_STATS_TXQ_STATS],
+					     txq_stats_policy) == 0) {
+
+				/* sum the backlogs over all TIDs for station */
+				if (txq_stats[NL80211_TXQ_STATS_BACKLOG_BYTES])
+					data->backlog_bytes += nla_get_u32(
+						txq_stats[NL80211_TXQ_STATS_BACKLOG_BYTES]);
+				if (txq_stats[NL80211_TXQ_STATS_BACKLOG_PACKETS])
+					data->backlog_bytes += nla_get_u32(
+						txq_stats[NL80211_TXQ_STATS_BACKLOG_PACKETS]);
+			}
+		}
+	}
+
+
 	return NL_SKIP;
 }
 
@@ -10877,6 +10932,7 @@  const struct wpa_driver_ops wpa_driver_nl80211_ops = {
 	.sta_remove = driver_nl80211_sta_remove,
 	.hapd_send_eapol = wpa_driver_nl80211_hapd_send_eapol,
 	.sta_set_flags = wpa_driver_nl80211_sta_set_flags,
+	.sta_set_airtime_weight = driver_nl80211_sta_set_airtime_weight,
 	.hapd_init = i802_init,
 	.hapd_deinit = i802_deinit,
 	.set_wds_sta = i802_set_wds_sta,