* [RFC PATCH 01/15] wifi: cfg80211: add the TCLAS element parsers
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
@ 2026-09-02 8:45 ` Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 02/15] wifi: cfg80211: add the flow parser and the MSCS key Felix Fietkau
` (14 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:45 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
The classifier of an SCS or MSCS request reaches the kernel as the raw
element it arrived as, so add the code that reads the element format.
cfg80211_parse_tclas() walks a chain of TCLAS elements and picks up the
TCLAS Processing element; cfg80211_parse_tclas_mask() reads TCLAS Mask
elements and returns the union of the parameters they select. Both share
one frame classifier parser, which is the only place that knows the
layouts.
They accept the classifier types 0, 4 and 5 and refuse the rest:
9.4.2.29 deprecates type 2 and type 1 for IPv6, type 4 covers what is
left of type 1, and the others either classify an MPDU or need an offset
located per packet. A refusal is a status code and not an error, which is
what 11.25.2 provides for. A User Priority other than 255 is refused too,
since it would make the UP of the MSDU a match parameter when for a
downlink classification it is the result.
The parsed values live in a struct cfg80211_flow_key, masked to the
parameters the classifier names, which is what the frame parser and the
MSCS lookup use as well.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/linux/ieee80211.h | 1 +-
include/net/cfg80211.h | 113 +++++++++-
net/wireless/Makefile | 2 +-
net/wireless/core.h | 6 +-
net/wireless/scs.c | 468 +++++++++++++++++++++++++++++++++++++++-
5 files changed, 589 insertions(+), 1 deletion(-)
create mode 100644 net/wireless/scs.c
diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h
index 26e674038865..1837a4e7ea30 100644
--- a/include/linux/ieee80211.h
+++ b/include/linux/ieee80211.h
@@ -1821,6 +1821,7 @@ enum ieee80211_eid_ext {
WLAN_EID_EXT_SHORT_SSID_LIST = 58,
WLAN_EID_EXT_HE_6GHZ_CAPA = 59,
WLAN_EID_EXT_UL_MU_POWER_CAPA = 60,
+ WLAN_EID_EXT_TCLAS_MASK = 89,
WLAN_EID_EXT_EHT_OPERATION = 106,
WLAN_EID_EXT_EHT_MULTI_LINK = 107,
WLAN_EID_EXT_EHT_CAPABILITY = 108,
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index 97c16d4ff127..795dd87b795e 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4068,6 +4068,119 @@ struct cfg80211_qos_map {
};
/**
+ * enum cfg80211_tclas_processing - how the TCLAS elements relate
+ *
+ * @CFG80211_TCLAS_PROCESSING_ALL: an MSDU has to match every element
+ * @CFG80211_TCLAS_PROCESSING_ANY: one matching element is enough
+ * @CFG80211_TCLAS_PROCESSING_DEFAULT: the stream claims what no other stream
+ * claims, and carries no element
+ * @CFG80211_TCLAS_PROCESSING_ABSENT: the descriptor carries no TCLAS
+ * Processing element. Not a value of the Processing field.
+ */
+enum cfg80211_tclas_processing {
+ CFG80211_TCLAS_PROCESSING_ALL = 0,
+ CFG80211_TCLAS_PROCESSING_ANY = 1,
+ CFG80211_TCLAS_PROCESSING_DEFAULT = 2,
+
+ CFG80211_TCLAS_PROCESSING_ABSENT = 0xff,
+};
+
+/**
+ * enum cfg80211_flow_field - classifier parameter of one MSDU
+ *
+ * A set of these is held as a bitmap of BIT(field) in a u32.
+ *
+ * @CFG80211_FLOW_F_ETH_SA: Ethernet source address
+ * @CFG80211_FLOW_F_ETH_DA: Ethernet destination address
+ * @CFG80211_FLOW_F_ETH_TYPE: ethertype
+ * @CFG80211_FLOW_F_VLAN_PCP: IEEE 802.1Q priority code point
+ * @CFG80211_FLOW_F_VLAN_DEI: IEEE 802.1Q drop eligibility indicator
+ * @CFG80211_FLOW_F_VLAN_VID: IEEE 802.1Q VLAN identifier
+ * @CFG80211_FLOW_F_IP_VERSION: IP version
+ * @CFG80211_FLOW_F_IP_SRC: IP source address
+ * @CFG80211_FLOW_F_IP_DST: IP destination address
+ * @CFG80211_FLOW_F_SRC_PORT: layer 4 source port
+ * @CFG80211_FLOW_F_DST_PORT: layer 4 destination port
+ * @CFG80211_FLOW_F_DSCP: differentiated services code point
+ * @CFG80211_FLOW_F_PROTO: IPv4 protocol or IPv6 next header
+ * @CFG80211_FLOW_F_FLOW_LABEL: IPv6 flow label
+ * @NUM_CFG80211_FLOW_FIELDS: number of fields
+ */
+enum cfg80211_flow_field {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+
+ NUM_CFG80211_FLOW_FIELDS,
+};
+
+/**
+ * struct cfg80211_flow_key - masked classifier parameters of one MSDU
+ *
+ * The MSCS lookup key. Every field the layout does not select is zero, and
+ * the whole structure is hashed and compared as a byte string, so it must
+ * hold no undefined octet.
+ *
+ * @src: IP source address, IPv4 in the first four octets
+ * @dst: IP destination address, IPv4 in the first four octets
+ * @flow_label: IPv6 flow label
+ * @sa: Ethernet source address
+ * @da: Ethernet destination address
+ * @eth_type: ethertype
+ * @vlan_tci: IEEE 802.1Q tag control information
+ * @src_port: layer 4 source port
+ * @dst_port: layer 4 destination port
+ * @ip_version: IP version, which keeps IPv4 and IPv6 keys apart
+ * @dscp: differentiated services code point, in the six LSBs
+ * @proto: IPv4 protocol or IPv6 next header
+ * @pad: must be zero
+ */
+struct cfg80211_flow_key {
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be32 flow_label;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 eth_type;
+ __be16 vlan_tci;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 ip_version;
+ u8 dscp;
+ u8 proto;
+ u8 pad;
+};
+
+/**
+ * struct cfg80211_tclas - parsed TCLAS element
+ *
+ * Only the classifier types 0, 4 and 5 are represented; see
+ * cfg80211_parse_tclas().
+ *
+ * @type: classifier type, see Table 9-203
+ * @fields: the parameters the classifier mask selects, as a bitmap of
+ * &enum cfg80211_flow_field
+ * @key: the values those parameters must have, with everything @fields does
+ * not select left zero, so a match is a comparison of two keys
+ */
+struct cfg80211_tclas {
+ u8 type;
+ u32 fields;
+ struct cfg80211_flow_key key;
+};
+
+/**
* DOC: Neighbor Awareness Networking (NAN)
*
* NAN uses two interface types:
diff --git a/net/wireless/Makefile b/net/wireless/Makefile
index a77fd5ba6368..552f4557ca23 100644
--- a/net/wireless/Makefile
+++ b/net/wireless/Makefile
@@ -8,7 +8,7 @@ obj-$(CONFIG_WEXT_PRIV) += wext-priv.o
cfg80211-y += core.o sysfs.o radiotap.o util.o reg.o scan.o nl80211.o
cfg80211-y += mlme.o ibss.o sme.o chan.o ethtool.o mesh.o ap.o trace.o ocb.o
-cfg80211-y += michael-mic.o pmsr.o
+cfg80211-y += michael-mic.o pmsr.o scs.o
cfg80211-$(CONFIG_OF) += of.o
cfg80211-$(CONFIG_CFG80211_DEBUGFS) += debugfs.o
cfg80211-$(CONFIG_CFG80211_WEXT) += wext-compat.o wext-sme.o
diff --git a/net/wireless/core.h b/net/wireless/core.h
index b4610f6685dc..6e8b9d651f4f 100644
--- a/net/wireless/core.h
+++ b/net/wireless/core.h
@@ -638,6 +638,12 @@ struct cfg80211_colocated_ap {
s8 psd_20;
};
+int cfg80211_tclas_count(const u8 *elems, size_t len);
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing);
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields);
+
#if IS_ENABLED(CONFIG_CFG80211_KUNIT_TEST)
#define EXPORT_SYMBOL_IF_CFG80211_KUNIT(sym) EXPORT_SYMBOL_IF_KUNIT(sym)
#define VISIBLE_IF_CFG80211_KUNIT
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
new file mode 100644
index 000000000000..b674d5aa180e
--- /dev/null
+++ b/net/wireless/scs.c
@@ -0,0 +1,468 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/visibility.h>
+#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/unaligned.h>
+#include <net/cfg80211.h>
+#include <net/dsfield.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
+#include "core.h"
+
+enum cfg80211_tclas_type {
+ CFG80211_TCLAS_ETH = 0,
+ CFG80211_TCLAS_IP = 4,
+ CFG80211_TCLAS_VLAN = 5,
+};
+
+/* Frame Classifier field, one layout per classifier type */
+struct tclas_fc {
+ u8 type;
+ u8 mask;
+} __packed;
+
+struct tclas_fc_eth {
+ struct tclas_fc hdr;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 ethertype;
+} __packed;
+
+struct tclas_fc_ip4 {
+ struct tclas_fc hdr;
+ u8 version;
+ __be32 src;
+ __be32 dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 reserved;
+} __packed;
+
+struct tclas_fc_ip6 {
+ struct tclas_fc hdr;
+ u8 version;
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 flow_label[3];
+} __packed;
+
+struct tclas_fc_vlan {
+ struct tclas_fc hdr;
+ u8 pcp;
+ u8 dei;
+ __be16 vid;
+} __packed;
+
+/* The key is hashed and compared as a byte string, so it must have no hole */
+static_assert(sizeof(struct cfg80211_flow_key) ==
+ 2 * sizeof(struct in6_addr) + sizeof(__be32) + 2 * ETH_ALEN +
+ 4 * sizeof(__be16) + 4);
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+#define FLOW_F_VLAN (FLOW_F(VLAN_PCP) | FLOW_F(VLAN_DEI) | FLOW_F(VLAN_VID))
+
+/*
+ * The parameter that each classifier mask bit selects, LSB first. A table
+ * length is also the number of parameters that the classifier type has, which
+ * is what drops the reserved bits above them.
+ */
+static const u8 tclas_map_eth[] = {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+};
+
+/* Both IP versions share this, and only IPv6 has the flow label */
+static const u8 tclas_map_ip[] = {
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+};
+
+static const u8 tclas_map_vlan[] = {
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+};
+
+static u32 tclas_mask_fields(u8 type, u8 version, u8 mask)
+{
+ size_t n = ARRAY_SIZE(tclas_map_ip);
+ const u8 *map = tclas_map_ip;
+ unsigned int i;
+ u32 fields = 0;
+
+ switch (type) {
+ case CFG80211_TCLAS_ETH:
+ map = tclas_map_eth;
+ n = ARRAY_SIZE(tclas_map_eth);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ map = tclas_map_vlan;
+ n = ARRAY_SIZE(tclas_map_vlan);
+ break;
+ default:
+ /* Only IPv6 has the last one, the flow label */
+ n -= version == 4;
+ break;
+ }
+
+ for (i = 0; i < n; i++)
+ if (mask & BIT(i))
+ fields |= BIT(map[i]);
+
+ return fields;
+}
+
+/* Copy the selected parameters, so that two keys compare as byte strings */
+static void flow_key_select(const struct cfg80211_flow_key *src, u32 fields,
+ struct cfg80211_flow_key *dst)
+{
+ memset(dst, 0, sizeof(*dst));
+
+ if (fields & FLOW_F(ETH_SA))
+ memcpy(dst->sa, src->sa, ETH_ALEN);
+ if (fields & FLOW_F(ETH_DA))
+ memcpy(dst->da, src->da, ETH_ALEN);
+ if (fields & FLOW_F(ETH_TYPE))
+ dst->eth_type = src->eth_type;
+
+ if (fields & FLOW_F_VLAN) {
+ u16 tci = 0;
+
+ if (fields & FLOW_F(VLAN_PCP))
+ tci |= VLAN_PRIO_MASK;
+ if (fields & FLOW_F(VLAN_DEI))
+ tci |= VLAN_CFI_MASK;
+ if (fields & FLOW_F(VLAN_VID))
+ tci |= VLAN_VID_MASK;
+
+ dst->vlan_tci = src->vlan_tci & htons(tci);
+ }
+
+ if (fields & FLOW_F(IP_VERSION))
+ dst->ip_version = src->ip_version;
+ if (fields & FLOW_F(IP_SRC))
+ dst->src = src->src;
+ if (fields & FLOW_F(IP_DST))
+ dst->dst = src->dst;
+ if (fields & FLOW_F(SRC_PORT))
+ dst->src_port = src->src_port;
+ if (fields & FLOW_F(DST_PORT))
+ dst->dst_port = src->dst_port;
+
+ if (fields & FLOW_F(DSCP))
+ dst->dscp = src->dscp;
+ if (fields & FLOW_F(PROTO))
+ dst->proto = src->proto;
+ if (fields & FLOW_F(FLOW_LABEL))
+ dst->flow_label = src->flow_label;
+}
+
+static int tclas_parse_eth(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_eth *fc = (const void *)data;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ memcpy(key->sa, fc->sa, ETH_ALEN);
+ memcpy(key->da, fc->da, ETH_ALEN);
+ key->eth_type = fc->ethertype;
+
+ return 0;
+}
+
+static int tclas_parse_ip4(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip4 *fc, bool values)
+{
+ if (values && fc->version != 4)
+ return -EINVAL;
+
+ key->ip_version = 4;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+
+ return 0;
+}
+
+static int tclas_parse_ip6(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip6 *fc, bool values)
+{
+ if (values && fc->version != 6)
+ return -EINVAL;
+
+ key->ip_version = 6;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+ key->flow_label =
+ cpu_to_be32(get_unaligned_be24(fc->flow_label) & 0xfffff);
+
+ return 0;
+}
+
+/*
+ * A TCLAS Mask element reserves the Version subfield, so the length is what
+ * says which layout this is. The two below are handed the layout it picked,
+ * so only this one sees octets.
+ */
+static int tclas_parse_ip(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len, bool values)
+{
+ if (len == sizeof(struct tclas_fc_ip4))
+ return tclas_parse_ip4(key, (const void *)data, values);
+
+ if (len == sizeof(struct tclas_fc_ip6))
+ return tclas_parse_ip6(key, (const void *)data, values);
+
+ return -EINVAL;
+}
+
+static int tclas_parse_vlan(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_vlan *fc = (const void *)data;
+ u16 tci;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ /*
+ * 9.4.2.29 gives the Priority Code Point four bits of value space
+ * where a tag carries three, so the element can name a value no frame
+ * has. Refuse one rather than fold it onto a value that matches.
+ */
+ if (fc->pcp > 7 || fc->dei > 1 || be16_to_cpu(fc->vid) > 0xfff)
+ return -EINVAL;
+
+ tci = fc->pcp << 13;
+ tci |= fc->dei << 12;
+ tci |= be16_to_cpu(fc->vid);
+ key->vlan_tci = cpu_to_be16(tci);
+
+ return 0;
+}
+
+/* @values is false for a TCLAS Mask element, which reserves every value */
+static int cfg80211_parse_frame_classifier(struct cfg80211_tclas *t,
+ const u8 *data, size_t len,
+ bool values)
+{
+ const struct tclas_fc *fc = (const void *)data;
+ struct cfg80211_flow_key raw = {};
+ int ret;
+
+ if (len < sizeof(*fc))
+ return -EINVAL;
+
+ memset(t, 0, sizeof(*t));
+ t->type = fc->type;
+
+ switch (t->type) {
+ case CFG80211_TCLAS_ETH:
+ ret = tclas_parse_eth(&raw, data, len);
+ break;
+ case CFG80211_TCLAS_IP:
+ ret = tclas_parse_ip(&raw, data, len, values);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ ret = tclas_parse_vlan(&raw, data, len);
+ break;
+ default:
+ /*
+ * The rest is deprecated, classifies an MPDU rather than an
+ * MSDU, or needs a window located per packet.
+ */
+ return -EOPNOTSUPP;
+ }
+
+ if (ret)
+ return ret;
+
+ t->fields = tclas_mask_fields(t->type, raw.ip_version, fc->mask);
+
+ /*
+ * The element's own length fixes the version, so comparing it can only
+ * refuse a packet of the other version whose address happens to equal
+ * the first octets of this one. 11.25.2 excludes it from the parameter
+ * count, not from the comparison.
+ */
+ if (t->type == CFG80211_TCLAS_IP)
+ t->fields |= FLOW_F(IP_VERSION);
+
+ flow_key_select(&raw, t->fields, &t->key);
+
+ return 0;
+}
+
+/**
+ * cfg80211_tclas_count - count the TCLAS elements of a chain
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ *
+ * Sizes the array that cfg80211_parse_tclas() fills. It counts what the chain
+ * claims and validates nothing.
+ *
+ * Return: the number of TCLAS elements, or -ENOSPC for more than a descriptor
+ * can hold.
+ */
+int cfg80211_tclas_count(const u8 *elems, size_t len)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+
+ for_each_element_id(elem, WLAN_EID_TCLAS, elems, len)
+ n++;
+
+ if (n > U8_MAX)
+ return -ENOSPC;
+
+ return n;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_tclas_count);
+
+/**
+ * cfg80211_parse_tclas - parse a chain of TCLAS elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @out: array that receives the parsed elements
+ * @n_tclas: number of entries in @out, from cfg80211_tclas_count()
+ * @processing: receives how the elements relate, or
+ * %CFG80211_TCLAS_PROCESSING_ABSENT when the chain carries no TCLAS
+ * Processing element
+ *
+ * The chain holds TCLAS elements (9.4.2.29) and at most one TCLAS Processing
+ * element (9.4.2.31). This is one of the two places in the kernel that reads
+ * the classifier element format.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier the kernel cannot evaluate, -ENOSPC for a chain that no
+ * longer fits @out.
+ */
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+ int ret;
+
+ *processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ for_each_element(elem, elems, len) {
+ switch (elem->id) {
+ case WLAN_EID_TCLAS:
+ if (n == n_tclas)
+ return -ENOSPC;
+
+ if (elem->datalen < 1)
+ return -EINVAL;
+
+ /*
+ * Any other value matches on the UP of the MSDU, which
+ * for a downlink classification is the result.
+ */
+ if (elem->data[0] != 255)
+ return -EOPNOTSUPP;
+
+ ret = cfg80211_parse_frame_classifier(&out[n],
+ elem->data + 1,
+ elem->datalen - 1,
+ true);
+ if (ret)
+ return ret;
+
+ n++;
+ break;
+ case WLAN_EID_TCLAS_PROCESSING:
+ /* One element, once, with a value the standard gives */
+ if (elem->datalen != 1 ||
+ *processing != CFG80211_TCLAS_PROCESSING_ABSENT ||
+ elem->data[0] > CFG80211_TCLAS_PROCESSING_DEFAULT)
+ return -EINVAL;
+
+ *processing = elem->data[0];
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas);
+
+/**
+ * cfg80211_parse_tclas_mask - parse a chain of TCLAS Mask elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @fields: receives the union of the selected classifier parameters, as a
+ * bitmap of &enum cfg80211_flow_field
+ *
+ * A TCLAS Mask element carries no classifier values, so the field selection is
+ * its whole content.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier type the kernel cannot evaluate.
+ */
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields)
+{
+ const struct element *elem;
+ struct cfg80211_tclas t;
+ int ret;
+
+ *fields = 0;
+
+ for_each_element(elem, elems, len) {
+ if (elem->id != WLAN_EID_EXTENSION || elem->datalen < 1 ||
+ elem->data[0] != WLAN_EID_EXT_TCLAS_MASK)
+ return -EINVAL;
+
+ ret = cfg80211_parse_frame_classifier(&t, elem->data + 1,
+ elem->datalen - 1, false);
+ if (ret)
+ return ret;
+
+ *fields |= t.fields;
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas_mask);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 02/15] wifi: cfg80211: add the flow parser and the MSCS key
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 01/15] wifi: cfg80211: add the TCLAS element parsers Felix Fietkau
@ 2026-09-02 8:45 ` Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 03/15] wifi: cfg80211: add the SCS evaluator Felix Fietkau
` (13 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:45 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
An SCS classifier is compared against the parameters of an MSDU, and an
MSCS lookup is keyed on them, so both need the frame read once into the
shape the classifier parser produces.
cfg80211_flow_parse() records which parameters the frame carries, because
a classifier that names one the frame lacks must not match. It reports
the ethertype behind any VLAN tag, so a tag the hardware stripped and an
inline one give the same answer, and for IPv6 the protocol of the header
itself rather than of whatever follows the extension headers.
It starts at skb->data instead of trusting skb->protocol or the network
header offset, because a frame the receive path forwards between two
stations arrives with skb->protocol set to ETH_P_802_3 and the network
header reset.
cfg80211_flow_key_build() masks the parsed frame down to the parameters
an MSCS names, and mirrors it when the caller asks: 11.25.3 item b) swaps
the addresses and the ports and nothing else.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/net/cfg80211.h | 31 ++++++-
net/wireless/scs.c | 228 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 259 insertions(+)
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index 795dd87b795e..db5f53147179 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4181,6 +4181,37 @@ struct cfg80211_tclas {
};
/**
+ * struct cfg80211_flow_info - classifier parameters of one MSDU
+ *
+ * @key: every parameter the source carries, with nothing masked out
+ * @present: bitmap of &enum cfg80211_flow_field, the parameters that the
+ * source really carries. A classifier that names an absent parameter
+ * does not match.
+ */
+struct cfg80211_flow_info {
+ struct cfg80211_flow_key key;
+ u32 present;
+};
+
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info);
+
+/**
+ * enum cfg80211_flow_dir - direction of a built flow key
+ *
+ * @CFG80211_FLOW_AS_IS: the key describes the frame that was parsed
+ * @CFG80211_FLOW_MIRRORED: the key describes the reverse direction, so the
+ * three pairs of IEEE Std 802.11-2024, 11.25.3 item b) are swapped
+ */
+enum cfg80211_flow_dir {
+ CFG80211_FLOW_AS_IS,
+ CFG80211_FLOW_MIRRORED,
+};
+
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key);
+
+/**
* DOC: Neighbor Awareness Networking (NAN)
*
* NAN uses two interface types:
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
index b674d5aa180e..06ba0a3a4184 100644
--- a/net/wireless/scs.c
+++ b/net/wireless/scs.c
@@ -466,3 +466,231 @@ int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields)
return 0;
}
EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas_mask);
+
+#define FLOW_F_PORTS (FLOW_F(SRC_PORT) | FLOW_F(DST_PORT))
+
+static void cfg80211_flow_parse_ports(struct sk_buff *skb, unsigned int offset,
+ int proto, struct cfg80211_flow_info *info)
+{
+ __be16 ports[2], *p;
+
+ switch (proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_UDP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return;
+ }
+
+ p = skb_header_pointer(skb, offset, sizeof(ports), ports);
+ if (!p)
+ return;
+
+ info->key.src_port = p[0];
+ info->key.dst_port = p[1];
+ info->present |= FLOW_F_PORTS;
+}
+
+static void cfg80211_flow_parse_ipv4(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct iphdr iph_buf, *iph;
+
+ iph = skb_header_pointer(skb, offset, sizeof(*iph), &iph_buf);
+ if (!iph || iph->version != 4 || iph->ihl < 5)
+ return;
+
+ info->key.ip_version = 4;
+ info->key.dscp = ipv4_get_dsfield(iph) >> 2;
+ info->key.proto = iph->protocol;
+ memcpy(&info->key.src, &iph->saddr, sizeof(iph->saddr));
+ memcpy(&info->key.dst, &iph->daddr, sizeof(iph->daddr));
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(PROTO);
+
+ if (iph->frag_off & htons(IP_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, offset + iph->ihl * 4, iph->protocol,
+ info);
+}
+
+static void cfg80211_flow_parse_ipv6(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct ipv6hdr ip6h_buf, *ip6h;
+ __be16 frag_off = 0;
+ int l4_off;
+ u8 nexthdr;
+
+ ip6h = skb_header_pointer(skb, offset, sizeof(*ip6h), &ip6h_buf);
+ if (!ip6h || ip6h->version != 6)
+ return;
+
+ info->key.ip_version = 6;
+ info->key.dscp = ipv6_get_dsfield(ip6h) >> 2;
+ info->key.flow_label = ip6_flowlabel(ip6h);
+ info->key.src = ip6h->saddr;
+ info->key.dst = ip6h->daddr;
+
+ /*
+ * The parameter is the field of the IPv6 header, not the upper layer
+ * protocol behind the extension headers.
+ */
+ info->key.proto = ip6h->nexthdr;
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(FLOW_LABEL) | FLOW_F(PROTO);
+
+ nexthdr = ip6h->nexthdr;
+ l4_off = ipv6_skip_exthdr(skb, offset + sizeof(*ip6h), &nexthdr,
+ &frag_off);
+ if (l4_off < 0 || frag_off & htons(IP6_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, l4_off, nexthdr, info);
+}
+
+/**
+ * cfg80211_flow_parse - read the classifier parameters of an MSDU
+ *
+ * @skb: the frame, in IEEE 802.3 format
+ * @info: receives the parameters
+ *
+ * The parser starts at skb->data and treats it as an Ethernet header. It uses
+ * neither skb->protocol nor skb->network_header, because a frame that the
+ * receive path forwards between two stations arrives with skb->protocol equal
+ * to ETH_P_802_3 and with the network header reset to the Ethernet header.
+ *
+ * Return: %true when @info describes the frame.
+ */
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info)
+{
+ struct ethhdr eth_buf, *eth;
+ unsigned int offset = ETH_HLEN;
+ __be16 proto;
+
+ memset(info, 0, sizeof(*info));
+
+ eth = skb_header_pointer(skb, 0, sizeof(*eth), ð_buf);
+ if (!eth)
+ return false;
+
+ ether_addr_copy(info->key.sa, eth->h_source);
+ ether_addr_copy(info->key.da, eth->h_dest);
+ info->present = FLOW_F(ETH_SA) | FLOW_F(ETH_DA);
+ proto = eth->h_proto;
+
+ if (skb_vlan_tag_present(skb)) {
+ info->key.vlan_tci = htons(skb_vlan_tag_get(skb));
+ info->present |= FLOW_F_VLAN;
+ } else if (eth_type_vlan(proto)) {
+ struct vlan_hdr vhdr_buf, *vhdr;
+
+ vhdr = skb_header_pointer(skb, offset, sizeof(*vhdr), &vhdr_buf);
+ if (!vhdr)
+ return false;
+
+ info->key.vlan_tci = vhdr->h_vlan_TCI;
+ info->present |= FLOW_F_VLAN;
+ proto = vhdr->h_vlan_encapsulated_proto;
+ offset += sizeof(*vhdr);
+ }
+
+ /*
+ * The parameter is the ethertype behind any VLAN tag, so that a tag the
+ * hardware stripped and an inline one give the same answer.
+ */
+ info->key.eth_type = proto;
+ info->present |= FLOW_F(ETH_TYPE);
+
+ switch (proto) {
+ case htons(ETH_P_IP):
+ cfg80211_flow_parse_ipv4(skb, offset, info);
+ break;
+ case htons(ETH_P_IPV6):
+ cfg80211_flow_parse_ipv6(skb, offset, info);
+ break;
+ }
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_parse);
+
+static void flow_key_swap(struct cfg80211_flow_key *k)
+{
+ u8 addr[ETH_ALEN];
+
+ memcpy(addr, k->sa, ETH_ALEN);
+ memcpy(k->sa, k->da, ETH_ALEN);
+ memcpy(k->da, addr, ETH_ALEN);
+
+ swap(k->src, k->dst);
+ swap(k->src_port, k->dst_port);
+}
+
+/* Only these three pairs mirror; every other parameter mirrors onto itself */
+static u32 cfg80211_flow_fields_mirror(u32 fields)
+{
+ static const u8 pairs[][2] = {
+ { CFG80211_FLOW_F_ETH_SA, CFG80211_FLOW_F_ETH_DA },
+ { CFG80211_FLOW_F_IP_SRC, CFG80211_FLOW_F_IP_DST },
+ { CFG80211_FLOW_F_SRC_PORT, CFG80211_FLOW_F_DST_PORT },
+ };
+ unsigned int i;
+ u32 out = fields;
+
+ for (i = 0; i < ARRAY_SIZE(pairs); i++) {
+ u32 lo = BIT(pairs[i][0]), hi = BIT(pairs[i][1]);
+
+ out &= ~(lo | hi);
+ if (fields & lo)
+ out |= hi;
+ if (fields & hi)
+ out |= lo;
+ }
+
+ return out;
+}
+
+/**
+ * cfg80211_flow_key_build - build an MSCS lookup key
+ *
+ * @info: parsed frame, from cfg80211_flow_parse()
+ * @fields: classifier parameters of the MSCS, a bitmap of
+ * &enum cfg80211_flow_field
+ * @dir: %CFG80211_FLOW_MIRRORED when @info describes an uplink frame and the
+ * key shall describe the downlink direction
+ * @key: receives the key
+ *
+ * Everything that @fields does not select stays zero, so the key is hashed and
+ * compared as a byte string.
+ *
+ * Return: %false when @info lacks a parameter that @fields selects, in which
+ * case the MSDU is not classified at all.
+ */
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key)
+{
+ struct cfg80211_flow_key mirrored;
+
+ if (dir == CFG80211_FLOW_AS_IS) {
+ if (fields & ~info->present)
+ return false;
+
+ flow_key_select(&info->key, fields, key);
+
+ return true;
+ }
+
+ if (cfg80211_flow_fields_mirror(fields) & ~info->present)
+ return false;
+
+ mirrored = info->key;
+ flow_key_swap(&mirrored);
+ flow_key_select(&mirrored, fields, key);
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_key_build);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 03/15] wifi: cfg80211: add the SCS evaluator
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 01/15] wifi: cfg80211: add the TCLAS element parsers Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 02/15] wifi: cfg80211: add the flow parser and the MSCS key Felix Fietkau
@ 2026-09-02 8:45 ` Felix Fietkau
2026-09-02 8:45 ` [RFC PATCH 04/15] wifi: cfg80211: add SCS and MSCS request validation Felix Fietkau
` (12 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:45 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
cfg80211_scs_evaluate() walks the descriptors of one peer and answers
with the user priority of the one that claims an MSDU. That is the whole
of SCS on the host side: everything before it parses, everything after it
stores.
When several descriptors match, 11.25.2 gives the frame to the one that
names the most classifier parameters, and an equal count keeps the first,
which the standard leaves to the implementation. Where the elements of a
descriptor are ORed, its count is the smallest of them, because that is
what a frame had to satisfy to match.
The verdict also carries an undecided bit, set when a descriptor named a
parameter the caller does not carry and did not match anyway. That means
nothing for a frame, and tells a caller answering per flow rather than
per frame that this flow cannot be decided from what it knows.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/net/cfg80211.h | 46 +++++++++++++++-
include/uapi/linux/nl80211.h | 18 ++++++-
net/wireless/scs.c | 111 ++++++++++++++++++++++++++++++++++++-
3 files changed, 175 insertions(+)
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index db5f53147179..c3b17ee6a192 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4212,6 +4212,52 @@ bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
struct cfg80211_flow_key *key);
/**
+ * struct cfg80211_scs_desc - one SCS descriptor
+ *
+ * @req_type: add, remove or change
+ * @tclas_processing: how the elements of @tclas relate
+ * @qos_char: the whole QoS Characteristics element, or %NULL. Set only once
+ * ieee80211_qos_char_size_ok() has passed, so the accessors in
+ * <linux/ieee80211-eht.h> may read it.
+ * @id: SCSID, 1 to 255
+ * @up: user priority to assign to a matching MSDU
+ * @qos_char_len: length of @qos_char
+ * @n_tclas: number of entries in @tclas
+ * @tclas: the classifier
+ */
+struct cfg80211_scs_desc {
+ enum nl80211_scs_req_type req_type;
+ enum cfg80211_tclas_processing tclas_processing;
+ const struct ieee80211_qos_char_elem *qos_char;
+ u8 id;
+ u8 up;
+ u16 qos_char_len;
+ u8 n_tclas;
+ struct cfg80211_tclas tclas[] __counted_by(n_tclas);
+};
+
+/**
+ * struct cfg80211_scs_verdict - result of an SCS evaluation
+ *
+ * @match: an SCS descriptor claimed the MSDU
+ * @undecided: a descriptor named a parameter the caller does not carry and
+ * did not match anyway. A frame is simply not classified; an offload
+ * caller cannot answer for the flow at all.
+ * @scsid: identifier of the matching descriptor
+ * @up: user priority to assign
+ */
+struct cfg80211_scs_verdict {
+ bool match;
+ bool undecided;
+ u8 scsid;
+ u8 up;
+};
+
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict);
+
+/**
* DOC: Neighbor Awareness Networking (NAN)
*
* NAN uses two interface types:
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 020387d76412..86e1855c8475 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -6066,6 +6066,24 @@ enum nl80211_tx_power_setting {
};
/**
+ * enum nl80211_scs_req_type - SCS or MSCS request type
+ *
+ * The values are those of the Request Type field, IEEE Std 802.11-2024,
+ * Table 9-286.
+ *
+ * @NL80211_SCS_REQ_ADD: create the stream, replacing an active one of the
+ * same identifier
+ * @NL80211_SCS_REQ_REMOVE: terminate the stream
+ * @NL80211_SCS_REQ_CHANGE: modify an active stream, leaving it in force if
+ * the request is declined
+ */
+enum nl80211_scs_req_type {
+ NL80211_SCS_REQ_ADD,
+ NL80211_SCS_REQ_REMOVE,
+ NL80211_SCS_REQ_CHANGE,
+};
+
+/**
* enum nl80211_tid_config - TID config state
* @NL80211_TID_CONFIG_ENABLE: Enable config for the TID
* @NL80211_TID_CONFIG_DISABLE: Disable config for the TID
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
index 06ba0a3a4184..299a8d83205e 100644
--- a/net/wireless/scs.c
+++ b/net/wireless/scs.c
@@ -694,3 +694,114 @@ bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
return true;
}
EXPORT_SYMBOL(cfg80211_flow_key_build);
+
+static bool tclas_match(const struct cfg80211_tclas *t,
+ const struct cfg80211_flow_info *info, bool *undecided)
+{
+ struct cfg80211_flow_key key;
+
+ if (!cfg80211_flow_key_build(info, t->fields, CFG80211_FLOW_AS_IS,
+ &key)) {
+ *undecided = true;
+
+ return false;
+ }
+
+ return !memcmp(&key, &t->key, sizeof(key));
+}
+
+static u16 tclas_param_count(const struct cfg80211_tclas *t)
+{
+ /* The count is fixed at 3 for classifier type 5 */
+ if (t->type == CFG80211_TCLAS_VLAN)
+ return 3;
+
+ /* The IP version is not counted, and no other type carries it */
+ return hweight32(t->fields & ~FLOW_F(IP_VERSION));
+}
+
+static bool scs_desc_match(const struct cfg80211_scs_desc *desc,
+ const struct cfg80211_flow_info *info,
+ bool *undecided)
+{
+ unsigned int i;
+ bool any = false;
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ if (tclas_match(&desc->tclas[i], info, undecided)) {
+ any = true;
+ continue;
+ }
+
+ if (desc->tclas_processing == CFG80211_TCLAS_PROCESSING_ALL)
+ return false;
+ }
+
+ return any;
+}
+
+static u16 scs_desc_param_count(const struct cfg80211_scs_desc *desc)
+{
+ unsigned int i;
+ u16 count;
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL) {
+ count = U16_MAX;
+ for (i = 0; i < desc->n_tclas; i++)
+ count = min(count, tclas_param_count(&desc->tclas[i]));
+
+ return count == U16_MAX ? 0 : count;
+ }
+
+ count = 0;
+ for (i = 0; i < desc->n_tclas; i++)
+ count += tclas_param_count(&desc->tclas[i]);
+
+ return count;
+}
+
+/**
+ * cfg80211_scs_evaluate - pick the SCS descriptor that claims a flow
+ *
+ * @desc: the active descriptors of one peer
+ * @n_desc: number of entries in @desc
+ * @info: the flow, from cfg80211_flow_parse()
+ * @verdict: receives the result
+ *
+ * When several descriptors match, the one that requires the greatest number of
+ * classifier parameters wins. An equal count keeps the first, which 11.25.2
+ * leaves to the implementation.
+ */
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict)
+{
+ unsigned int i;
+ int best = -1;
+
+ memset(verdict, 0, sizeof(*verdict));
+
+ for (i = 0; i < n_desc; i++) {
+ bool undecided = false;
+ u16 count;
+
+ if (!scs_desc_match(desc[i], info, &undecided)) {
+ /*
+ * A descriptor that matched is decided, whichever
+ * element was skipped. Only this one leaves a question.
+ */
+ verdict->undecided |= undecided;
+ continue;
+ }
+
+ count = scs_desc_param_count(desc[i]);
+ if (count <= best)
+ continue;
+
+ best = count;
+ verdict->match = true;
+ verdict->scsid = desc[i]->id;
+ verdict->up = desc[i]->up;
+ }
+}
+EXPORT_SYMBOL(cfg80211_scs_evaluate);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 04/15] wifi: cfg80211: add SCS and MSCS request validation
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (2 preceding siblings ...)
2026-09-02 8:45 ` [RFC PATCH 03/15] wifi: cfg80211: add the SCS evaluator Felix Fietkau
@ 2026-09-02 8:45 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 05/15] wifi: cfg80211: add set_scs and set_mscs driver ops Felix Fietkau
` (11 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:45 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Check what the element parsers leave: the rules of 9.4.2.120 and 9.4.2.242
relating the request type, the classifier and the traffic description to
each other.
A failed check is a status code and not an error. The request came from a
peer over the air and hostapd passes it on as it arrived, so an AP answers
a legal request with a legal refusal.
The one rule the standard leaves open is a TCLAS Processing 0 descriptor
whose elements name one parameter with two values. No MSDU can satisfy
it, so decline it rather than install a rule that never fires.
An uplink or direct link descriptor carries no classifier, so add the QoS
Characteristics element accessors that read the direction, and the size
check that has to pass before anything else reads one.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/linux/ieee80211-eht.h | 128 +++++++++++++++++++++++++++++++++++-
include/linux/ieee80211.h | 1 +-
include/net/cfg80211.h | 17 +++++-
net/wireless/core.h | 2 +-
net/wireless/scs.c | 110 ++++++++++++++++++++++++++++++-
5 files changed, 258 insertions(+)
diff --git a/include/linux/ieee80211-eht.h b/include/linux/ieee80211-eht.h
index b62297a978e7..73c1b441c7f3 100644
--- a/include/linux/ieee80211-eht.h
+++ b/include/linux/ieee80211-eht.h
@@ -1369,4 +1369,132 @@ static inline u32 ieee80211_eml_trans_timeout_in_us(u16 eml_cap)
_data + ieee80211_mle_common_size(_data),\
_len - ieee80211_mle_common_size(_data))
+/**
+ * struct ieee80211_qos_char_elem - QoS Characteristics element
+ * @element_id: %WLAN_EID_EXTENSION
+ * @length: length of everything behind it
+ * @element_id_ext: %WLAN_EID_EXT_QOS_CHARACTERISTICS
+ * @control: control info, see %IEEE80211_QOS_CHAR_CTRL_*
+ * @min_service_interval: minimum service period interval, in microseconds
+ * @max_service_interval: maximum service period interval, in microseconds
+ * @min_data_rate: minimum data rate at the MAC SAP, in kilobits per second
+ * @delay_bound: targeted transport time of an MSDU, in microseconds
+ * @variable: the optional parameters that the presence bitmap names, in the
+ * order of the %IEEE80211_QOS_CHAR_PRES_* bits
+ *
+ * This is the whole element, header included, because the only consumer of it
+ * is a device that takes it as it arrived.
+ */
+struct ieee80211_qos_char_elem {
+ u8 element_id;
+ u8 length;
+ u8 element_id_ext;
+ __le32 control;
+ __le32 min_service_interval;
+ __le32 max_service_interval;
+ u8 min_data_rate[3];
+ u8 delay_bound[3];
+ u8 variable[];
+} __packed;
+
+#define IEEE80211_QOS_CHAR_CTRL_DIRECTION GENMASK(1, 0)
+#define IEEE80211_QOS_CHAR_CTRL_PRESENCE GENMASK(24, 9)
+
+#define IEEE80211_QOS_CHAR_DIR_UPLINK 0
+#define IEEE80211_QOS_CHAR_DIR_DOWNLINK 1
+#define IEEE80211_QOS_CHAR_DIR_DIRECT 2
+
+#define IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE BIT(0)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME BIT(1)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID BIT(2)
+#define IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE BIT(3)
+#define IEEE80211_QOS_CHAR_PRES_BURST_SIZE BIT(4)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME BIT(5)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO BIT(6)
+#define IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME BIT(7)
+
+/**
+ * ieee80211_qos_char_presence - QoS Characteristics presence bitmap
+ * @qc: the element
+ * Return: the Presence Bitmap Of Additional Parameters subfield, a bitmap of
+ * %IEEE80211_QOS_CHAR_PRES_*
+ */
+static inline u16
+ieee80211_qos_char_presence(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_PRESENCE);
+}
+
+/**
+ * ieee80211_qos_char_direction - QoS Characteristics direction
+ * @qc: the element
+ * Return: %IEEE80211_QOS_CHAR_DIR_UPLINK, _DOWNLINK or _DIRECT
+ */
+static inline u8
+ieee80211_qos_char_direction(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_DIRECTION);
+}
+
+/**
+ * ieee80211_qos_char_size_ok - check a QoS Characteristics element
+ * @data: candidate octets, from the Element ID
+ * @len: length of @data
+ *
+ * Takes octets rather than an element, because deciding whether they are one
+ * is what it is for. Every other accessor takes the element, so it can only be
+ * read once this has passed.
+ *
+ * Return: %true if the octets are the element they claim to be and its length
+ * matches the parameters it says are present
+ */
+static inline bool ieee80211_qos_char_size_ok(const u8 *data, size_t len)
+{
+ const struct ieee80211_qos_char_elem *qc = (const void *)data;
+ size_t needed = sizeof(*qc);
+ u16 present;
+
+ if (len < needed)
+ return false;
+
+ if (qc->element_id != WLAN_EID_EXTENSION ||
+ qc->element_id_ext != WLAN_EID_EXT_QOS_CHARACTERISTICS ||
+ qc->length != len - 2)
+ return false;
+
+ present = ieee80211_qos_char_presence(qc);
+
+ if (present & ~GENMASK(7, 0))
+ return false;
+
+ if (ieee80211_qos_char_direction(qc) > IEEE80211_QOS_CHAR_DIR_DIRECT)
+ return false;
+
+ /* A LinkID names the link of the Service Start Time, so it needs one */
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID &&
+ !(present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME))
+ return false;
+
+ if (present & IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE)
+ needed += 3;
+ if (present & IEEE80211_QOS_CHAR_PRES_BURST_SIZE)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME)
+ needed += 2;
+
+ return len == needed;
+}
+
#endif /* LINUX_IEEE80211_EHT_H */
diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h
index 1837a4e7ea30..6a515df36fc9 100644
--- a/include/linux/ieee80211.h
+++ b/include/linux/ieee80211.h
@@ -1826,6 +1826,7 @@ enum ieee80211_eid_ext {
WLAN_EID_EXT_EHT_MULTI_LINK = 107,
WLAN_EID_EXT_EHT_CAPABILITY = 108,
WLAN_EID_EXT_TID_TO_LINK_MAPPING = 109,
+ WLAN_EID_EXT_QOS_CHARACTERISTICS = 113,
WLAN_EID_EXT_BANDWIDTH_INDICATION = 135,
WLAN_EID_EXT_KNOWN_STA_IDENTIFCATION = 136,
WLAN_EID_EXT_NON_AP_STA_REG_CON = 137,
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index c3b17ee6a192..f140c3c2a5b8 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4237,6 +4237,23 @@ struct cfg80211_scs_desc {
};
/**
+ * struct cfg80211_mscs_desc - the MSCS of one peer
+ *
+ * @req_type: add, remove or change
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority, 0 to 7
+ * @stream_timeout: minimum lifetime of a learned value, in TUs
+ * @fields: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ */
+struct cfg80211_mscs_desc {
+ enum nl80211_scs_req_type req_type;
+ u8 up_bitmap;
+ u8 up_limit;
+ u32 stream_timeout;
+ u32 fields;
+};
+
+/**
* struct cfg80211_scs_verdict - result of an SCS evaluation
*
* @match: an SCS descriptor claimed the MSDU
diff --git a/net/wireless/core.h b/net/wireless/core.h
index 6e8b9d651f4f..2d6d0d70abce 100644
--- a/net/wireless/core.h
+++ b/net/wireless/core.h
@@ -643,6 +643,8 @@ int cfg80211_parse_tclas(const u8 *elems, size_t len,
struct cfg80211_tclas *out, u8 n_tclas,
enum cfg80211_tclas_processing *processing);
int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields);
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc);
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc);
#if IS_ENABLED(CONFIG_CFG80211_KUNIT_TEST)
#define EXPORT_SYMBOL_IF_CFG80211_KUNIT(sym) EXPORT_SYMBOL_IF_KUNIT(sym)
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
index 299a8d83205e..a8daaa99b49e 100644
--- a/net/wireless/scs.c
+++ b/net/wireless/scs.c
@@ -805,3 +805,113 @@ void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
}
}
EXPORT_SYMBOL(cfg80211_scs_evaluate);
+
+/*
+ * Two elements of one Processing 0 descriptor can demand two values for one
+ * parameter, which no MSDU satisfies.
+ */
+static bool tclas_conflict(const struct cfg80211_tclas *a,
+ const struct cfg80211_tclas *b)
+{
+ struct cfg80211_flow_key ka, kb;
+ u32 common = a->fields & b->fields;
+
+ if (!common)
+ return false;
+
+ flow_key_select(&a->key, common, &ka);
+ flow_key_select(&b->key, common, &kb);
+
+ return memcmp(&ka, &kb, sizeof(ka));
+}
+
+/**
+ * cfg80211_scs_desc_valid - check one SCS descriptor against the standard
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * The checks that a classifier element can fail on its own live in
+ * cfg80211_parse_tclas(). This is what is left: the rules that relate the
+ * request type, the classifier and the traffic description to each other.
+ *
+ * Return: whether the descriptor may be installed. A refusal is a legal
+ * answer to a legal request, so the caller declines it with a status
+ * code rather than failing the message.
+ */
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc)
+{
+ bool want_tclas = true;
+ unsigned int i, j;
+
+ /*
+ * 11.25.2 answers a termination with TCLAS_PROCESSING_TERMINATED and
+ * makes no exception for a malformed one, and it offers no denial
+ * status for one either. A removal names an identifier, so anything
+ * else the station put in the descriptor changes nothing it means.
+ */
+ if (desc->req_type == NL80211_SCS_REQ_REMOVE)
+ return true;
+
+ /* An uplink or direct link descriptor is a traffic description */
+ if (desc->qos_char &&
+ ieee80211_qos_char_direction(desc->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ want_tclas = false;
+
+ if (want_tclas != !!desc->n_tclas)
+ return false;
+
+ /* Such a descriptor holds no classifier, so it relates nothing */
+ if (!want_tclas)
+ return desc->tclas_processing == CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ switch (desc->tclas_processing) {
+ case CFG80211_TCLAS_PROCESSING_ALL:
+ case CFG80211_TCLAS_PROCESSING_ANY:
+ /* A choice between elements needs more than one element */
+ if (desc->n_tclas < 2)
+ return false;
+ break;
+ case CFG80211_TCLAS_PROCESSING_DEFAULT:
+ /* 9.4.2.120 pairs it with no classifier, and there is one */
+ return false;
+ case CFG80211_TCLAS_PROCESSING_ABSENT:
+ break;
+ default:
+ return false;
+ }
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL)
+ return true;
+
+ for (i = 0; i < desc->n_tclas; i++)
+ for (j = i + 1; j < desc->n_tclas; j++)
+ if (tclas_conflict(&desc->tclas[i], &desc->tclas[j]))
+ return false;
+
+ return true;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_scs_desc_valid);
+
+/**
+ * cfg80211_mscs_desc_valid - check an MSCS descriptor against the standard
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * Return: whether the descriptor may be installed.
+ */
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc)
+{
+ bool remove = desc->req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!desc->fields)
+ return false;
+
+ /*
+ * The Stream Timeout is the least time the AP keeps a learned value.
+ * Zero asks for no guarantee at all, so nothing would ever expire the
+ * entries of that station and its own table would fill with values for
+ * flows that ended. 9.4.2.242 reserves the field for a Remove.
+ */
+ return remove || desc->stream_timeout;
+}
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 05/15] wifi: cfg80211: add set_scs and set_mscs driver ops
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (3 preceding siblings ...)
2026-09-02 8:45 ` [RFC PATCH 04/15] wifi: cfg80211: add SCS and MSCS request validation Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 06/15] wifi: cfg80211: add SCS and MSCS configuration commands Felix Fietkau
` (10 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Add the two ops that install a stream classification on a peer, the
extended feature flags that advertise them, and a wiphy_register() check
that a wiphy does not advertise what it cannot do.
One SCS Request frame can carry several descriptors with different
request types, so set_scs takes a set and each descriptor carries its own
status back. A peer has at most one active MSCS, so set_mscs needs only a
return value.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/net/cfg80211.h | 28 +++++++++++++++++++++-
include/uapi/linux/nl80211.h | 10 ++++++++-
net/wireless/core.c | 10 ++++++++-
net/wireless/rdev-ops.h | 32 ++++++++++++++++++++++++-
net/wireless/trace.h | 49 +++++++++++++++++++++++++++++++++++++-
5 files changed, 129 insertions(+)
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index f140c3c2a5b8..73ccb6e3173c 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4237,6 +4237,18 @@ struct cfg80211_scs_desc {
};
/**
+ * struct cfg80211_scs_result - what one SCS descriptor is answered with
+ *
+ * Parallel to the descriptor array, so a descriptor holds what was asked for
+ * and never what came of it.
+ *
+ * @status: IEEE status code
+ */
+struct cfg80211_scs_result {
+ u16 status;
+};
+
+/**
* struct cfg80211_mscs_desc - the MSCS of one peer
*
* @req_type: add, remove or change
@@ -5359,6 +5371,16 @@ struct mgmt_frame_regs {
*
* @set_qos_map: Set QoS mapping information to the driver
*
+ * @set_scs: Add, change or remove SCS descriptors of one peer. The action
+ * frame exchange has been handled by userspace, so this just has to make
+ * the transmit path classify matching MSDUs. The op answers every
+ * descriptor in the result entry of the same index, and a declined change
+ * leaves the previously accepted classification of that SCSID in force.
+ * A negative return fails the request as a whole and answers nothing.
+ * @set_mscs: Install, change or remove the MSCS of one peer. A peer has at
+ * most one active MSCS, so the result is a single return value rather
+ * than a per descriptor status.
+ *
* @set_ap_chanwidth: Set the AP (including P2P GO) mode channel width for the
* given interface This is used e.g. for dynamic HT 20/40 MHz channel width
* changes during the lifetime of the BSS.
@@ -5763,6 +5785,12 @@ struct cfg80211_ops {
struct net_device *dev,
struct cfg80211_qos_map *qos_map);
+ int (*set_scs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+ int (*set_mscs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+
int (*set_ap_chanwidth)(struct wiphy *wiphy, struct net_device *dev,
unsigned int link_id,
struct cfg80211_chan_def *chandef);
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 86e1855c8475..f3e085c3c34c 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -7127,6 +7127,14 @@ enum nl80211_feature_flags {
*
* @NL80211_EXT_FEATURE_PROBE_AP: Driver supports probing the associated AP
* in STA mode using @NL80211_CMD_PROBE_PEER.
+ * @NL80211_EXT_FEATURE_SCS: Driver or device supports the stream
+ * classification service, so it classifies transmitted MSDUs against the
+ * descriptors given with %NL80211_CMD_SET_SCS. Userspace sets the SCS
+ * field of the Extended Capabilities element only when this is reported.
+ * @NL80211_EXT_FEATURE_MSCS: Driver or device supports the mirrored stream
+ * classification service, configured with %NL80211_CMD_SET_MSCS.
+ * Userspace sets the Mirrored SCS field of the Extended Capabilities
+ * element only when this is reported.
*
* @NUM_NL80211_EXT_FEATURES: number of extended features.
* @MAX_NL80211_EXT_FEATURES: highest extended feature index.
@@ -7210,6 +7218,8 @@ enum nl80211_ext_feature_index {
NL80211_EXT_FEATURE_ROC_ADDR_FILTER,
NL80211_EXT_FEATURE_SET_KEY_LTF_SEED,
NL80211_EXT_FEATURE_PROBE_AP,
+ NL80211_EXT_FEATURE_SCS,
+ NL80211_EXT_FEATURE_MSCS,
/* add new features before the definition below */
NUM_NL80211_EXT_FEATURES,
diff --git a/net/wireless/core.c b/net/wireless/core.c
index d13310fef691..cc2cff43ef75 100644
--- a/net/wireless/core.c
+++ b/net/wireless/core.c
@@ -974,6 +974,16 @@ int wiphy_register(struct wiphy *wiphy)
rdev->ops->update_connect_params))
return -EINVAL;
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_SCS) &&
+ !rdev->ops->set_scs))
+ return -EINVAL;
+
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_MSCS) &&
+ !rdev->ops->set_mscs))
+ return -EINVAL;
+
if (wiphy->addresses)
memcpy(wiphy->perm_addr, wiphy->addresses[0].addr, ETH_ALEN);
diff --git a/net/wireless/rdev-ops.h b/net/wireless/rdev-ops.h
index 46849fe8d0b3..1287c3542d93 100644
--- a/net/wireless/rdev-ops.h
+++ b/net/wireless/rdev-ops.h
@@ -1185,6 +1185,38 @@ static inline int rdev_set_qos_map(struct cfg80211_registered_device *rdev,
return ret;
}
+static inline int rdev_set_scs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_scs) {
+ trace_rdev_set_scs(&rdev->wiphy, dev, peer, n_desc);
+ ret = rdev->ops->set_scs(&rdev->wiphy, dev, peer, desc, res,
+ n_desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
+static inline int rdev_set_mscs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_mscs_desc *desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_mscs) {
+ trace_rdev_set_mscs(&rdev->wiphy, dev, peer, desc);
+ ret = rdev->ops->set_mscs(&rdev->wiphy, dev, peer, desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
static inline int
rdev_set_ap_chanwidth(struct cfg80211_registered_device *rdev,
struct net_device *dev,
diff --git a/net/wireless/trace.h b/net/wireless/trace.h
index 8c2a91b85c39..81c0899cb212 100644
--- a/net/wireless/trace.h
+++ b/net/wireless/trace.h
@@ -2636,6 +2636,55 @@ TRACE_EVENT(rdev_set_ap_chanwidth,
__entry->link_id)
);
+TRACE_EVENT(rdev_set_scs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, u8 n_desc),
+ TP_ARGS(wiphy, netdev, peer, n_desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u8, n_desc)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->n_desc = n_desc;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT ", %pM, %d descriptors",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->n_desc)
+);
+
+TRACE_EVENT(rdev_set_mscs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, const struct cfg80211_mscs_desc *desc),
+ TP_ARGS(wiphy, netdev, peer, desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u32, fields)
+ __field(u32, stream_timeout)
+ __field(u8, up_bitmap)
+ __field(u8, up_limit)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->fields = desc ? desc->fields : 0;
+ __entry->stream_timeout = desc ? desc->stream_timeout : 0;
+ __entry->up_bitmap = desc ? desc->up_bitmap : 0;
+ __entry->up_limit = desc ? desc->up_limit : 0;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT
+ ", %pM, fields %x, UP bitmap %x, UP limit %d, timeout %d",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->fields,
+ __entry->up_bitmap, __entry->up_limit,
+ __entry->stream_timeout)
+);
+
TRACE_EVENT(rdev_add_tx_ts,
TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
u8 tsid, const u8 *peer, u8 user_prio, u16 admitted_time),
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 06/15] wifi: cfg80211: add SCS and MSCS configuration commands
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (4 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 05/15] wifi: cfg80211: add set_scs and set_mscs driver ops Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 07/15] wifi: cfg80211: add KUnit tests for the SCS classifier Felix Fietkau
` (9 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Add NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS, which install on one
peer the stream classification it asked for. Userspace exchanges the
action frames and decides policy; these carry the result down.
The classifier travels as the raw element it arrived as, because nothing
in this path takes it apart. That also decides what is an error and what
is a refusal: an element came from the air, so a malformed or unsupported
one is answered with a status code in the reply, and a netlink error is
kept for what the caller built itself. Add the three status codes of
Table 9-80 that this maps onto.
A request is refused unless the wiphy advertises the matching extended
feature.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/linux/ieee80211.h | 4 +-
include/uapi/linux/nl80211.h | 100 ++++++++-
net/wireless/nl80211.c | 426 ++++++++++++++++++++++++++++++++++++-
3 files changed, 530 insertions(+)
diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h
index 6a515df36fc9..28630f8d2535 100644
--- a/include/linux/ieee80211.h
+++ b/include/linux/ieee80211.h
@@ -1526,6 +1526,10 @@ enum ieee80211_statuscode {
WLAN_STATUS_DENIED_TID_TO_LINK_MAPPING = 133,
WLAN_STATUS_PREF_TID_TO_LINK_MAPPING_SUGGESTED = 134,
WLAN_STATUS_8021X_AUTH_SUCCESS = 153,
+ /* SCS and MSCS */
+ WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP = 56,
+ WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES = 57,
+ WLAN_STATUS_TCLAS_PROCESSING_TERMINATED = 97,
};
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index f3e085c3c34c..e63eba2ee506 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -1427,6 +1427,24 @@
* @NL80211_CMD_STOP_PD: Stop the PD operation, identified by
* its %NL80211_ATTR_WDEV interface.
*
+ * @NL80211_CMD_SET_SCS: Add, change or remove stream classification service
+ * descriptors of the peer given by %NL80211_ATTR_MAC. The descriptors
+ * are in %NL80211_ATTR_SCS_DESCRIPTORS, and one message can carry
+ * several with different request types, because one SCS Request frame
+ * can. Userspace builds and parses the action frames and holds the
+ * dialog tokens. The reply repeats %NL80211_ATTR_SCS_DESCRIPTORS with
+ * the identifier and the IEEE status code of each descriptor, so that
+ * userspace can build the SCS Status List directly. The netlink return
+ * code stays 0 when the kernel processed the message, even if it
+ * declined single descriptors.
+ * @NL80211_CMD_SET_MSCS: Install, change or remove the mirrored stream
+ * classification service of the peer given by %NL80211_ATTR_MAC. The
+ * parameters are in %NL80211_ATTR_MSCS_DESCRIPTOR. A peer has at most
+ * one active MSCS, so the reply carries a single
+ * %NL80211_ATTR_STATUS_CODE. The same command serves both setup paths:
+ * an MSCS Request frame, and a (Re)Association Request frame that
+ * carried an MSCS Descriptor element.
+ *
* @NL80211_CMD_MAX: highest used command number
* @__NL80211_CMD_AFTER_LAST: internal use
*/
@@ -1705,6 +1723,9 @@ enum nl80211_commands {
NL80211_CMD_START_PD,
NL80211_CMD_STOP_PD,
+ NL80211_CMD_SET_SCS,
+ NL80211_CMD_SET_MSCS,
+
/* add new commands above here */
/* used to define NL80211_CMD_MAX below */
@@ -3184,6 +3205,11 @@ enum nl80211_commands {
*
* The aggregated message always precedes the per-link messages for the
* same station within a dump sequence.
+ * @NL80211_ATTR_SCS_DESCRIPTORS: Nested array of SCS descriptors, each one a
+ * nested set of &enum nl80211_scs_desc_attr attributes. Used with
+ * %NL80211_CMD_SET_SCS in both directions.
+ * @NL80211_ATTR_MSCS_DESCRIPTOR: Nested set of &enum nl80211_mscs_desc_attr
+ * attributes. Used with %NL80211_CMD_SET_MSCS.
*
* @NUM_NL80211_ATTR: total number of nl80211_attrs available
* @NL80211_ATTR_MAX: highest attribute number currently defined
@@ -3784,6 +3810,8 @@ enum nl80211_attrs {
NL80211_ATTR_NPCA_PUNCT_BITMAP,
NL80211_ATTR_STA_DUMP_LINK_STATS,
+ NL80211_ATTR_SCS_DESCRIPTORS,
+ NL80211_ATTR_MSCS_DESCRIPTOR,
/* add attributes here, update the policy in nl80211.c */
@@ -6084,6 +6112,78 @@ enum nl80211_scs_req_type {
};
/**
+ * enum nl80211_scs_desc_attr - one SCS descriptor
+ *
+ * The scalars that carry policy are typed attributes, because they are the
+ * values that a bad request can make nonsensical, and netlink policy checks
+ * them before any code runs. The classifier travels as the raw element it
+ * arrived as, because no userspace component in this path takes it apart:
+ * wpa_supplicant holds it opaque, and a Multi-AP Agent moves it between
+ * agents verbatim.
+ *
+ * @__NL80211_SCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_SCS_DESC_ATTR_ID: SCSID (u8, 1 to 255)
+ * @NL80211_SCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_SCS_DESC_ATTR_UP: user priority to assign to a matching MSDU
+ * (u8, 0 to 7), taken from the Intra-Access Category Priority element.
+ * Required for add and change, and rejected for remove and for a
+ * descriptor whose %NL80211_SCS_DESC_ATTR_QOS_CHAR gives a direction
+ * other than downlink, which carries no such element.
+ * @NL80211_SCS_DESC_ATTR_TCLAS: one or more TCLAS elements and at most one
+ * TCLAS Processing element (binary), as they arrived over the air
+ * @NL80211_SCS_DESC_ATTR_QOS_CHAR: QoS Characteristics element (binary), whole
+ * and as it arrived over the air, header included
+ * @NL80211_SCS_DESC_ATTR_STATUS: IEEE status code (u16), kernel to userspace
+ *
+ * @__NL80211_SCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_SCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_scs_desc_attr {
+ __NL80211_SCS_DESC_ATTR_INVALID,
+ NL80211_SCS_DESC_ATTR_ID,
+ NL80211_SCS_DESC_ATTR_REQ_TYPE,
+ NL80211_SCS_DESC_ATTR_UP,
+ NL80211_SCS_DESC_ATTR_TCLAS,
+ NL80211_SCS_DESC_ATTR_QOS_CHAR,
+ NL80211_SCS_DESC_ATTR_STATUS,
+
+ __NL80211_SCS_DESC_ATTR_AFTER_LAST,
+ NL80211_SCS_DESC_ATTR_MAX = __NL80211_SCS_DESC_ATTR_AFTER_LAST - 1
+};
+
+/**
+ * enum nl80211_mscs_desc_attr - the MSCS of one peer
+ *
+ * @__NL80211_MSCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_MSCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_MSCS_DESC_ATTR_UP_BITMAP: user priorities that the AP learns
+ * from, one bit each (u8)
+ * @NL80211_MSCS_DESC_ATTR_UP_LIMIT: ceiling for the assigned user priority
+ * (u8, 0 to 7)
+ * @NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT: minimum lifetime of a learned
+ * value, in TUs (u32)
+ * @NL80211_MSCS_DESC_ATTR_TCLAS_MASK: one or more TCLAS Mask elements
+ * (binary), as they arrived over the air. Required for add and change,
+ * and rejected for remove.
+ *
+ * @__NL80211_MSCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_MSCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_mscs_desc_attr {
+ __NL80211_MSCS_DESC_ATTR_INVALID,
+ NL80211_MSCS_DESC_ATTR_REQ_TYPE,
+ NL80211_MSCS_DESC_ATTR_UP_BITMAP,
+ NL80211_MSCS_DESC_ATTR_UP_LIMIT,
+ NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT,
+ NL80211_MSCS_DESC_ATTR_TCLAS_MASK,
+
+ __NL80211_MSCS_DESC_ATTR_AFTER_LAST,
+ NL80211_MSCS_DESC_ATTR_MAX = __NL80211_MSCS_DESC_ATTR_AFTER_LAST - 1
+};
+
+/**
* enum nl80211_tid_config - TID config state
* @NL80211_TID_CONFIG_ENABLE: Enable config for the TID
* @NL80211_TID_CONFIG_DISABLE: Disable config for the TID
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 44f2bad08670..cce843850fd3 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -623,6 +623,32 @@ sar_policy[NL80211_SAR_ATTR_MAX + 1] = {
[NL80211_SAR_ATTR_SPECS] = NLA_POLICY_NESTED_ARRAY(sar_specs_policy),
};
+/*
+ * The raw element attributes carry what arrived over the air, and the code
+ * that reads them validates it. A length check here would answer a malformed
+ * element with a netlink error instead of the status code it is due.
+ */
+static const struct nla_policy
+nl80211_scs_desc_policy[NL80211_SCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_SCS_DESC_ATTR_ID] = NLA_POLICY_MIN(NLA_U8, 1),
+ [NL80211_SCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_SCS_DESC_ATTR_UP] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_SCS_DESC_ATTR_TCLAS] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_QOS_CHAR] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_STATUS] = { .type = NLA_REJECT },
+};
+
+static const struct nla_policy
+nl80211_mscs_desc_policy[NL80211_MSCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_MSCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_MSCS_DESC_ATTR_UP_BITMAP] = { .type = NLA_U8 },
+ [NL80211_MSCS_DESC_ATTR_UP_LIMIT] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT] = { .type = NLA_U32 },
+ [NL80211_MSCS_DESC_ATTR_TCLAS_MASK] = { .type = NLA_BINARY },
+};
+
static const struct nla_policy
nl80211_mbssid_config_policy[NL80211_MBSSID_CONFIG_ATTR_MAX + 1] = {
[NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES] = NLA_POLICY_MIN(NLA_U8, 2),
@@ -1096,6 +1122,10 @@ static const struct nla_policy nl80211_policy[NUM_NL80211_ATTR] = {
[NL80211_ATTR_NPCA_PUNCT_BITMAP] =
NLA_POLICY_FULL_RANGE(NLA_U32, &nl80211_punct_bitmap_range),
[NL80211_ATTR_STA_DUMP_LINK_STATS] = { .type = NLA_FLAG },
+ [NL80211_ATTR_SCS_DESCRIPTORS] =
+ NLA_POLICY_NESTED_ARRAY(nl80211_scs_desc_policy),
+ [NL80211_ATTR_MSCS_DESCRIPTOR] =
+ NLA_POLICY_NESTED(nl80211_mscs_desc_policy),
};
/* policy for the key attributes */
@@ -18256,6 +18286,390 @@ static int nl80211_set_qos_map(struct sk_buff *skb,
return ret;
}
+/* The SCS Descriptor List field of one SCS Request frame cannot exceed this */
+#define NL80211_MAX_SCS_DESC 255
+
+static int nl80211_scs_iftype_ok(struct net_device *dev)
+{
+ switch (dev->ieee80211_ptr->iftype) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_P2P_GO:
+ case NL80211_IFTYPE_STATION:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/*
+ * A raw element came from the air and hostapd passes it on unchanged, so a bad
+ * one is declined rather than rejected as a malformed message.
+ */
+static u16 nl80211_scs_status(int err)
+{
+ switch (err) {
+ case -EOPNOTSUPP:
+ return WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP;
+ case -ENOSPC:
+ return WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES;
+ default:
+ return WLAN_STATUS_REQUEST_DECLINED;
+ }
+}
+
+static void nl80211_free_scs_desc(struct cfg80211_scs_desc **desc, unsigned int n)
+{
+ unsigned int i;
+
+ for (i = 0; i < n; i++)
+ kfree(desc[i]);
+
+ kfree(desc);
+}
+
+static struct cfg80211_scs_desc *
+nl80211_parse_scs_desc(struct nlattr *attr, struct cfg80211_scs_result *res,
+ struct genl_info *info)
+{
+ enum cfg80211_tclas_processing processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+ struct nlattr *tb[NL80211_SCS_DESC_ATTR_MAX + 1];
+ const struct ieee80211_qos_char_elem *qos_char = NULL;
+ struct nlattr *qos_attr, *tclas_attr;
+ struct cfg80211_scs_desc *desc;
+ bool wants_up = true;
+ int n_tclas = 0;
+ bool remove;
+ int err;
+
+ err = nla_parse_nested(tb, NL80211_SCS_DESC_ATTR_MAX, attr,
+ nl80211_scs_desc_policy, info->extack);
+ if (err)
+ return ERR_PTR(err);
+
+ if (!tb[NL80211_SCS_DESC_ATTR_ID] ||
+ !tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info, "SCS descriptor without an ID or a request type");
+ return ERR_PTR(-EINVAL);
+ }
+
+ remove = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) ==
+ NL80211_SCS_REQ_REMOVE;
+
+ qos_attr = tb[NL80211_SCS_DESC_ATTR_QOS_CHAR];
+ /* Only a candidate that passes is an element anyone may read */
+ if (qos_attr && ieee80211_qos_char_size_ok(nla_data(qos_attr),
+ nla_len(qos_attr)))
+ qos_char = nla_data(qos_attr);
+
+ /*
+ * An uplink or direct link descriptor carries no Intra-Access Category
+ * Priority element, so userspace has no user priority to unpack from
+ * one.
+ */
+ if (qos_char && ieee80211_qos_char_direction(qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ wants_up = false;
+
+ /*
+ * The elements that decide this came from the air, so a descriptor
+ * that contradicts them is declined below and the rest of the request
+ * stands. A malformed element hides the direction, and is declined
+ * too. A removal is answered whatever it carries, per 11.25.2.
+ */
+ if (!remove && (!qos_attr || qos_char) &&
+ wants_up != !!tb[NL80211_SCS_DESC_ATTR_UP])
+ err = -EINVAL;
+
+ tclas_attr = tb[NL80211_SCS_DESC_ATTR_TCLAS];
+ if (!err && tclas_attr) {
+ n_tclas = cfg80211_tclas_count(nla_data(tclas_attr),
+ nla_len(tclas_attr));
+ if (n_tclas < 0) {
+ err = n_tclas;
+ n_tclas = 0;
+ }
+ }
+
+ desc = kzalloc(struct_size(desc, tclas, n_tclas), GFP_KERNEL);
+ if (!desc)
+ return ERR_PTR(-ENOMEM);
+
+ desc->id = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_ID]);
+ desc->req_type = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]);
+ desc->up = nla_get_u8_default(tb[NL80211_SCS_DESC_ATTR_UP], 0);
+ desc->n_tclas = n_tclas;
+
+ if (!err && tclas_attr) {
+ err = cfg80211_parse_tclas(nla_data(tclas_attr),
+ nla_len(tclas_attr), desc->tclas,
+ n_tclas, &processing);
+ if (err) {
+ desc->n_tclas = 0;
+ processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+ }
+ }
+
+ desc->tclas_processing = processing;
+
+ if (!err && qos_attr) {
+ if (!qos_char) {
+ NL_SET_ERR_MSG(info->extack,
+ "bad QoS Characteristics element");
+ err = -EINVAL;
+ } else {
+ desc->qos_char = qos_char;
+ desc->qos_char_len = nla_len(qos_attr);
+ }
+ }
+
+ if (err)
+ res->status = nl80211_scs_status(err);
+ else if (!cfg80211_scs_desc_valid(desc))
+ res->status = WLAN_STATUS_REQUEST_DECLINED;
+
+ return desc;
+}
+
+static int nl80211_set_scs_reply(struct genl_info *info,
+ struct cfg80211_scs_desc * const *desc,
+ const struct cfg80211_scs_result *res,
+ unsigned int n_desc)
+{
+ struct nlattr *list, *entry;
+ struct sk_buff *msg;
+ unsigned int i;
+ size_t size;
+ void *hdr;
+
+ /* Identifier and status, for up to 255 descriptors */
+ size = nla_total_size(nla_total_size(sizeof(u8)) +
+ nla_total_size(sizeof(u16)));
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE + n_desc * size, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0,
+ NL80211_CMD_SET_SCS);
+ if (!hdr)
+ goto nla_put_failure;
+
+ list = nla_nest_start(msg, NL80211_ATTR_SCS_DESCRIPTORS);
+ if (!list)
+ goto nla_put_failure;
+
+ for (i = 0; i < n_desc; i++) {
+ entry = nla_nest_start(msg, i + 1);
+ if (!entry)
+ goto nla_put_failure;
+
+ if (nla_put_u8(msg, NL80211_SCS_DESC_ATTR_ID, desc[i]->id) ||
+ nla_put_u16(msg, NL80211_SCS_DESC_ATTR_STATUS,
+ res[i].status))
+ goto nla_put_failure;
+
+ nla_nest_end(msg, entry);
+ }
+
+ nla_nest_end(msg, list);
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+
+nla_put_failure:
+ nlmsg_free(msg);
+
+ return -ENOBUFS;
+}
+
+static int nl80211_set_scs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct net_device *dev = info->user_ptr[1];
+ unsigned int i, n_desc = 0, n_apply = 0;
+ struct cfg80211_scs_result *res;
+ struct cfg80211_scs_desc **desc;
+ struct nlattr *nla;
+ int rem, err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_SCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_SCS_DESCRIPTORS])
+ return -EINVAL;
+
+ /* The result is a user priority, which applies to every link */
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info, "SCS rules belong to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_scs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS], rem)
+ n_desc++;
+
+ if (!n_desc || n_desc > NL80211_MAX_SCS_DESC)
+ return -EINVAL;
+
+ desc = kcalloc(n_desc, sizeof(*desc), GFP_KERNEL);
+ if (!desc)
+ return -ENOMEM;
+
+ res = kcalloc(n_desc, sizeof(*res), GFP_KERNEL);
+ if (!res) {
+ kfree(desc);
+ return -ENOMEM;
+ }
+
+ i = 0;
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS],
+ rem) {
+ desc[i] = nl80211_parse_scs_desc(nla, &res[i], info);
+ if (IS_ERR(desc[i])) {
+ err = PTR_ERR(desc[i]);
+ desc[i] = NULL;
+ goto out;
+ }
+
+ i++;
+ }
+
+ /*
+ * The driver is given the unanswered descriptors alone. Gathering them
+ * at the front keeps each one at the index of its result, and a reply
+ * entry carries its own SCSID, so the order means nothing.
+ */
+ for (i = 0; i < n_desc; i++) {
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
+ swap(desc[i], desc[n_apply]);
+ swap(res[i], res[n_apply]);
+ n_apply++;
+ }
+
+ if (n_apply) {
+ err = rdev_set_scs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]),
+ desc, res, n_apply);
+ if (err)
+ goto out;
+ }
+
+ err = nl80211_set_scs_reply(info, desc, res, n_desc);
+
+out:
+ kfree(res);
+ nl80211_free_scs_desc(desc, n_desc);
+
+ return err;
+}
+
+static int nl80211_send_status_code(struct genl_info *info, u32 cmd, u16 status)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0, cmd);
+ if (!hdr || nla_put_u16(msg, NL80211_ATTR_STATUS_CODE, status)) {
+ nlmsg_free(msg);
+ return -ENOBUFS;
+ }
+
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+}
+
+static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct nlattr *tb[NL80211_MSCS_DESC_ATTR_MAX + 1];
+ struct net_device *dev = info->user_ptr[1];
+ struct cfg80211_mscs_desc desc = {};
+ u16 status = WLAN_STATUS_SUCCESS;
+ struct nlattr *mask;
+ bool remove;
+ int err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_MSCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR])
+ return -EINVAL;
+
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info, "an MSCS belongs to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_scs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ err = nla_parse_nested(tb, NL80211_MSCS_DESC_ATTR_MAX,
+ info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR],
+ nl80211_mscs_desc_policy, info->extack);
+ if (err)
+ return err;
+
+ if (!tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info, "MSCS descriptor without a request type");
+ return -EINVAL;
+ }
+
+ desc.req_type = nla_get_u8(tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]);
+ remove = desc.req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK]) {
+ GENL_SET_ERR_MSG(info, "TCLAS Mask elements do not match the request type");
+ return -EINVAL;
+ }
+
+ desc.up_bitmap = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_BITMAP],
+ 0);
+ desc.up_limit = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_LIMIT],
+ 7);
+ desc.stream_timeout =
+ nla_get_u32_default(tb[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT], 0);
+
+ mask = tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK];
+ if (mask) {
+ err = cfg80211_parse_tclas_mask(nla_data(mask), nla_len(mask),
+ &desc.fields);
+ if (err) {
+ status = nl80211_scs_status(err);
+ goto reply;
+ }
+ }
+
+ if (!cfg80211_mscs_desc_valid(&desc)) {
+ status = WLAN_STATUS_REQUEST_DECLINED;
+ goto reply;
+ }
+
+ err = rdev_set_mscs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]), &desc);
+ if (err)
+ return err;
+
+ /* 11.25.3 gives an accepted termination a status of its own */
+ if (remove)
+ status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+reply:
+ return nl80211_send_status_code(info, NL80211_CMD_SET_MSCS, status);
+}
+
static int nl80211_add_tx_ts(struct sk_buff *skb, struct genl_info *info)
{
struct cfg80211_registered_device *rdev = info->user_ptr[0];
@@ -20409,6 +20823,18 @@ static const struct genl_small_ops nl80211_small_ops[] = {
.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
},
{
+ .cmd = NL80211_CMD_SET_SCS,
+ .doit = nl80211_set_scs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
+ {
+ .cmd = NL80211_CMD_SET_MSCS,
+ .doit = nl80211_set_mscs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
+ {
.cmd = NL80211_CMD_ADD_TX_TS,
.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
.doit = nl80211_add_tx_ts,
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 07/15] wifi: cfg80211: add KUnit tests for the SCS classifier
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (5 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 06/15] wifi: cfg80211: add SCS and MSCS configuration commands Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 08/15] wifi: mac80211: add SCS and MSCS rule storage Felix Fietkau
` (8 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Cover the cases where a mistake gives a wrong priority rather than a
crash, so that nothing else would report it:
- A non-first IP fragment must report no ports.
- A descriptor the frame cannot satisfy must not match, one it can must,
and the granularity rule must decide between two that both match,
whatever order they are in.
- A descriptor that matched must leave nothing undecided.
- A mirrored key must swap the pairs of 11.25.3 item b) and leave every
unselected field zero.
- A refused classifier type must be refused.
- The QoS Characteristics presence bitmap must place every optional field
at the right offset.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/wireless/tests/Makefile | 2 +-
net/wireless/tests/scs.c | 547 +++++++++++++++++++++++++++++++++++++-
2 files changed, 548 insertions(+), 1 deletion(-)
create mode 100644 net/wireless/tests/scs.c
diff --git a/net/wireless/tests/Makefile b/net/wireless/tests/Makefile
index c364e63b508e..a4b7f2e206fc 100644
--- a/net/wireless/tests/Makefile
+++ b/net/wireless/tests/Makefile
@@ -1,3 +1,3 @@
-cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o
+cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o scs.o
obj-$(CONFIG_CFG80211_KUNIT_TEST) += cfg80211-tests.o
diff --git a/net/wireless/tests/scs.c b/net/wireless/tests/scs.c
new file mode 100644
index 000000000000..f3f237a7d924
--- /dev/null
+++ b/net/wireless/tests/scs.c
@@ -0,0 +1,547 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for the SCS and MSCS classifier
+ *
+ * Each case is one that fails with a wrong priority rather than a crash.
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/skbuff.h>
+#include <kunit/test.h>
+#include <linux/ieee80211.h>
+#include <linux/ip.h>
+#include <net/cfg80211.h>
+#include "../core.h"
+
+static const u8 test_da[ETH_ALEN] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 };
+
+/* An Ethernet header, an IPv4 header and four octets of UDP ports */
+static const u8 udp_frame[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x00,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+/* The same datagram, as a fragment with a nonzero offset */
+static const u8 udp_fragment[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x02,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+static struct sk_buff *test_skb(struct kunit *test, const u8 *data, size_t len)
+{
+ struct sk_buff *skb = kunit_zalloc_skb(test, 256, GFP_KERNEL);
+
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put_data(skb, data, len);
+
+ return skb;
+}
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+/* Reading the layer 4 header of a later fragment would match payload octets */
+static void flow_parse_fragment(struct kunit *test)
+{
+ struct cfg80211_flow_info info;
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_frame,
+ sizeof(udp_frame)),
+ &info));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 5000);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 10000);
+ KUNIT_EXPECT_EQ(test, info.key.proto, IPPROTO_UDP);
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_fragment,
+ sizeof(udp_fragment)),
+ &info));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, info.key.src_port, 0);
+ KUNIT_EXPECT_EQ(test, info.key.dst_port, 0);
+
+ /* The addresses are still classifier parameters of a later fragment */
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_SRC));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_DST));
+}
+
+static struct cfg80211_scs_desc *
+test_scs_desc(struct kunit *test, u8 id, u8 up, const u8 *elems, size_t len)
+{
+ struct cfg80211_scs_desc *desc;
+ int n_tclas;
+
+ n_tclas = cfg80211_tclas_count(elems, len);
+ KUNIT_ASSERT_GE(test, n_tclas, 0);
+
+ desc = kunit_kzalloc(test, struct_size(desc, tclas, n_tclas),
+ GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, desc);
+
+ desc->id = id;
+ desc->up = up;
+ desc->n_tclas = n_tclas;
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas(elems, len, desc->tclas, n_tclas,
+ &desc->tclas_processing));
+
+ return desc;
+}
+
+/* When two descriptors match, the more granular one wins */
+static void scs_granularity(struct kunit *test)
+{
+ /* Classifier type 4 over IPv4, source and destination address */
+ static const u8 two_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x06, /* type, mask: src and dst address */
+ 4, /* version */
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0, /* ports, not selected */
+ 0, 0, 0, /* dscp, protocol, reserved */
+ };
+ /* The same, and the destination port as well */
+ static const u8 three_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x16, /* src, dst, dst port */
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0x27, 0x10,
+ 0, 0, 0,
+ };
+ /* A source port that the frame does not carry a matching value for */
+ static const u8 wrong_port[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x08, /* src port only */
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0x00, 0x35, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_scs_desc *list[3];
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+
+ list[0] = test_scs_desc(test, 1, 3, two_params, sizeof(two_params));
+ list[1] = test_scs_desc(test, 2, 6, three_params, sizeof(three_params));
+ list[2] = test_scs_desc(test, 3, 7, wrong_port, sizeof(wrong_port));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_frame,
+ sizeof(udp_frame)),
+ &info));
+
+ /*
+ * Neither mask sets the version bit, but a type 4 classifier compares
+ * the version anyway. It still does not count, so the counts are 2 and
+ * 3 and the descriptor order below is what the rule decides.
+ */
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST));
+ KUNIT_EXPECT_EQ(test, list[1]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DST_PORT));
+
+ /* Only the two address descriptors match, and the granular one wins */
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+ KUNIT_EXPECT_EQ(test, verdict.up, 6);
+
+ /* The order of the list must not decide it */
+ swap(list[0], list[1]);
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+
+ /* A frame without ports leaves only the address descriptor */
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_fragment,
+ sizeof(udp_fragment)),
+ &info));
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 1);
+ KUNIT_EXPECT_EQ(test, verdict.up, 3);
+ KUNIT_EXPECT_TRUE(test, verdict.undecided);
+}
+
+/* A key learned from an uplink frame must describe the downlink direction */
+static void flow_key_mirror(struct kunit *test)
+{
+ /* Classifier type 4 over IPv4, both addresses and both ports */
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 19,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 4, 0x1e,
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_flow_key as_is, mirrored;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(SRC_PORT) | FLOW_F(DST_PORT));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_frame,
+ sizeof(udp_frame)),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_AS_IS,
+ &as_is));
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &mirrored));
+
+ KUNIT_EXPECT_MEMEQ(test, &as_is.src, &mirrored.dst, sizeof(as_is.src));
+ KUNIT_EXPECT_MEMEQ(test, &as_is.dst, &mirrored.src, sizeof(as_is.dst));
+ KUNIT_EXPECT_EQ(test, as_is.src_port, mirrored.dst_port);
+ KUNIT_EXPECT_EQ(test, as_is.dst_port, mirrored.src_port);
+
+ /* Nothing the layout does not select may reach the key */
+ KUNIT_EXPECT_MEMEQ(test, as_is.sa, "\x00\x00\x00\x00\x00\x00",
+ ETH_ALEN);
+ KUNIT_EXPECT_EQ(test, as_is.eth_type, 0);
+ KUNIT_EXPECT_EQ(test, as_is.proto, 0);
+
+ /* The version is one the layout does select, so it is in both keys */
+ KUNIT_EXPECT_EQ(test, as_is.ip_version, 4);
+ KUNIT_EXPECT_EQ(test, mirrored.ip_version, 4);
+}
+
+/* The Ethernet addresses mirror as a pair */
+static void flow_key_mirror_eth(struct kunit *test)
+{
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 17,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 0, 0x01, /* classifier type 0, source address */
+ 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0,
+ 0, 0,
+ };
+ struct cfg80211_flow_key key;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields, FLOW_F(ETH_SA));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_frame,
+ sizeof(udp_frame)),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &key));
+
+ /* The mirror parameter of the source address is the destination */
+ KUNIT_EXPECT_MEMEQ(test, key.sa, test_da, ETH_ALEN);
+ KUNIT_EXPECT_MEMEQ(test, key.da, "\x00\x00\x00\x00\x00\x00", ETH_ALEN);
+}
+
+/* Classifier types that classify an MPDU, and type 3 in a mask, are refused */
+static void tclas_refused(struct kunit *test)
+{
+ static const u8 mac_header[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 6, 0x00, 0x00, 0x00, /* type 6, three octet classifier mask */
+ };
+ static const u8 up_not_255[] = {
+ WLAN_EID_TCLAS, 17,
+ 0, /* a UP that takes part in the match */
+ 0, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02,
+ 0x08, 0x00,
+ };
+ static const u8 filter_in_mask[] = {
+ WLAN_EID_EXTENSION, 9,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 3, 0,
+ 0, 0,
+ 0x00, 0x00,
+ 0xff, 0xff,
+ };
+ /* Classifier type 1, superseded by type 4 and deprecated for IPv6 */
+ static const u8 legacy_ip[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 1, 0x06,
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ /* Classifier type 2, deprecated in favour of type 5 */
+ static const u8 legacy_vlan[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 2, 0x01,
+ 0x00, 0x64,
+ };
+ enum cfg80211_tclas_processing processing;
+ struct cfg80211_tclas tclas;
+ u32 fields;
+
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(mac_header, sizeof(mac_header),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(up_not_255, sizeof(up_not_255),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_ip, sizeof(legacy_ip),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_vlan, sizeof(legacy_vlan),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas_mask(filter_in_mask,
+ sizeof(filter_in_mask),
+ &fields));
+}
+
+/* A descriptor that matched is decided, whichever element was skipped */
+static void scs_undecided(struct kunit *test)
+{
+ /* A source address element and a source port element, match either */
+ static const u8 addr_or_port[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x02, /* source address */
+ 4,
+ 192, 168, 1, 1,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x08, /* source port */
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0x13, 0x88, 0, 0,
+ 0, 0, 0,
+
+ WLAN_EID_TCLAS_PROCESSING, 1, 1,
+ };
+ /* The source port alone */
+ static const u8 port_only[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x08,
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0x13, 0x88, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+ struct cfg80211_scs_desc *list[1];
+
+ list[0] = test_scs_desc(test, 1, 4, addr_or_port, sizeof(addr_or_port));
+ KUNIT_ASSERT_EQ(test, list[0]->n_tclas, 2);
+ KUNIT_ASSERT_EQ(test, list[0]->tclas_processing, 1);
+
+ /* Both elements match a whole datagram */
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_frame,
+ sizeof(udp_frame)),
+ &info));
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_FALSE(test, verdict.undecided);
+
+ /*
+ * A later fragment has no ports, so the port element is skipped. The
+ * address element still matches, so the descriptor is decided.
+ */
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_parse(test_skb(test, udp_fragment,
+ sizeof(udp_fragment)),
+ &info));
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_FALSE(test, verdict.undecided);
+
+ /* With nothing else to match on, the same fragment leaves it open */
+ list[0] = test_scs_desc(test, 2, 4, port_only, sizeof(port_only));
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_FALSE(test, verdict.match);
+ KUNIT_EXPECT_TRUE(test, verdict.undecided);
+}
+
+/* The presence bitmap decides where every optional field starts */
+static void qos_characteristics(struct kunit *test)
+{
+ /* Downlink, TID 5, UP 5, and a Service Start Time with no LinkID */
+ static const u8 no_link_id[] = {
+ 0xff, 0x17, 0x71, /* element header, 113 */
+ 0x55, 0x05, 0x00, 0x00, /* control info, presence bit 1 */
+ 0xe8, 0x03, 0x00, 0x00, /* minimum service interval */
+ 0xd0, 0x07, 0x00, 0x00, /* maximum service interval */
+ 0x88, 0x13, 0x00, /* minimum data rate */
+ 0x20, 0x4e, 0x00, /* delay bound */
+ 0x78, 0x56, 0x34, 0x12, /* service start time */
+ };
+ static const u8 with_link_id[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x0d, 0x00, 0x00, /* presence bits 1 and 2 */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ 0x03, /* service start time LinkID */
+ };
+ /* A LinkID with no Service Start Time refers to a field that is absent */
+ static const u8 orphan_link_id[] = {
+ 0xff, 0x14, 0x71,
+ 0x55, 0x09, 0x00, 0x00, /* presence bit 2 alone */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x03,
+ };
+ /* The header names the element, so a wrong extension is not this one */
+ static const u8 bad_ext_id[] = {
+ 0xff, 0x17, 0x70,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ /* The Length field must agree with the octets behind it */
+ static const u8 bad_length[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ /* The length must match the parameters the bitmap says are present */
+ static const u8 short_elem[] = {
+ 0xff, 0x13, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ };
+
+ KUNIT_EXPECT_TRUE(test, ieee80211_qos_char_size_ok(no_link_id,
+ sizeof(no_link_id)));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_direction((const void *)no_link_id),
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK);
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence((const void *)no_link_id),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME);
+
+ KUNIT_EXPECT_TRUE(test, ieee80211_qos_char_size_ok(with_link_id,
+ sizeof(with_link_id)));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence((const void *)with_link_id),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME |
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID);
+
+ KUNIT_EXPECT_FALSE(test, ieee80211_qos_char_size_ok(orphan_link_id,
+ sizeof(orphan_link_id)));
+ KUNIT_EXPECT_FALSE(test, ieee80211_qos_char_size_ok(short_elem,
+ sizeof(short_elem)));
+ KUNIT_EXPECT_FALSE(test, ieee80211_qos_char_size_ok(bad_ext_id,
+ sizeof(bad_ext_id)));
+ KUNIT_EXPECT_FALSE(test, ieee80211_qos_char_size_ok(bad_length,
+ sizeof(bad_length)));
+}
+
+/*
+ * An uplink or direct link descriptor carries no classifier and no user
+ * priority, per IEEE Std 802.11be-2024, 35.17. Refusing one would leave the
+ * traffic description that a driver schedules against unreachable.
+ */
+static void scs_traffic_description(struct kunit *test)
+{
+ /* Uplink, and a Service Start Time with no LinkID */
+ static const u8 uplink[] = {
+ 0xff, 0x17, 0x71,
+ 0x54, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ struct cfg80211_scs_desc desc = {
+ .req_type = NL80211_SCS_REQ_ADD,
+ .tclas_processing = CFG80211_TCLAS_PROCESSING_ABSENT,
+ .qos_char = (const void *)uplink,
+ .qos_char_len = sizeof(uplink),
+ .id = 1,
+ };
+
+ KUNIT_ASSERT_TRUE(test, ieee80211_qos_char_size_ok(uplink,
+ sizeof(uplink)));
+ KUNIT_ASSERT_EQ(test, ieee80211_qos_char_direction((const void *)uplink),
+ IEEE80211_QOS_CHAR_DIR_UPLINK);
+
+ KUNIT_EXPECT_TRUE(test, cfg80211_scs_desc_valid(&desc));
+
+ /* 35.17 bans the TCLAS Processing element from such a descriptor */
+ desc.tclas_processing = CFG80211_TCLAS_PROCESSING_DEFAULT;
+ KUNIT_EXPECT_FALSE(test, cfg80211_scs_desc_valid(&desc));
+}
+
+static struct kunit_case scs_cases[] = {
+ KUNIT_CASE(flow_parse_fragment),
+ KUNIT_CASE(scs_granularity),
+ KUNIT_CASE(scs_undecided),
+ KUNIT_CASE(flow_key_mirror),
+ KUNIT_CASE(flow_key_mirror_eth),
+ KUNIT_CASE(tclas_refused),
+ KUNIT_CASE(qos_characteristics),
+ KUNIT_CASE(scs_traffic_description),
+ {}
+};
+
+static struct kunit_suite scs = {
+ .name = "cfg80211-scs",
+ .test_cases = scs_cases,
+};
+
+kunit_test_suite(scs);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 08/15] wifi: mac80211: add SCS and MSCS rule storage
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (6 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 07/15] wifi: cfg80211: add KUnit tests for the SCS classifier Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 09/15] wifi: mac80211: classify transmitted MSDUs with SCS Felix Fietkau
` (7 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Implement the set_scs and set_mscs cfg80211 ops.
A request edits the rule set of one station, so the result mixes its
descriptors with those of earlier requests. mac80211 therefore rebuilds
the set whole, into one allocation that owns every rule it holds. That
gives the set a single lifetime and keeps the ownership rule simple.
An accepted removal answers with TCLAS_PROCESSING_TERMINATED, which the
standard reserves for it.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/mac80211/Makefile | 3 +-
net/mac80211/cfg.c | 2 +-
net/mac80211/ieee80211_i.h | 8 +-
net/mac80211/scs.c | 220 ++++++++++++++++++++++++++++++++++++++-
net/mac80211/sta_info.c | 1 +-
net/mac80211/sta_info.h | 38 +++++++-
6 files changed, 271 insertions(+), 1 deletion(-)
create mode 100644 net/mac80211/scs.c
diff --git a/net/mac80211/Makefile b/net/mac80211/Makefile
index 36f1e3e2222b..8f8729e44c5f 100644
--- a/net/mac80211/Makefile
+++ b/net/mac80211/Makefile
@@ -35,7 +35,8 @@ mac80211-y := \
tdls.o \
ocb.o \
airtime.o \
- eht.o uhr.o nan.o
+ eht.o uhr.o nan.o \
+ scs.o
mac80211-$(CONFIG_MAC80211_LEDS) += led.o
mac80211-$(CONFIG_MAC80211_DEBUGFS) += \
diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c
index 23f4f9ec86d0..aea6bce2afa6 100644
--- a/net/mac80211/cfg.c
+++ b/net/mac80211/cfg.c
@@ -6059,6 +6059,8 @@ const struct cfg80211_ops mac80211_config_ops = {
.end_cac = ieee80211_end_cac,
.channel_switch = ieee80211_channel_switch,
.set_qos_map = ieee80211_set_qos_map,
+ .set_scs = ieee80211_set_scs,
+ .set_mscs = ieee80211_set_mscs,
.set_ap_chanwidth = ieee80211_set_ap_chanwidth,
.add_tx_ts = ieee80211_add_tx_ts,
.del_tx_ts = ieee80211_del_tx_ts,
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 5761e9621491..31a630e68a2b 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -2204,6 +2204,14 @@ void ieee80211_recalc_txpower(struct ieee80211_link_data *link,
bool update_bss);
void ieee80211_recalc_offload(struct ieee80211_local *local);
+/* SCS and MSCS */
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+void ieee80211_sta_scs_free(struct sta_info *sta);
+
/* link handling */
void ieee80211_link_setup(struct ieee80211_link_data *link);
void ieee80211_link_init(struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
new file mode 100644
index 000000000000..d2daefb8e90c
--- /dev/null
+++ b/net/mac80211/scs.c
@@ -0,0 +1,220 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <linux/ieee80211.h>
+#include <net/cfg80211.h>
+#include "ieee80211_i.h"
+#include "sta_info.h"
+
+/* An hour, in TUs of 1024 microseconds */
+#define IEEE80211_MSCS_TIMEOUT_MAX_TU 3515625
+
+static size_t ieee80211_scs_rule_size(const struct cfg80211_scs_desc *desc)
+{
+ return ALIGN(struct_size(desc, tclas, desc->n_tclas) +
+ desc->qos_char_len, __alignof__(*desc));
+}
+
+/* Copy one rule to @pos and return the aligned octet after it */
+static void *ieee80211_scs_rule_copy(const struct cfg80211_scs_desc *src,
+ void *pos)
+{
+ size_t len = struct_size(src, tclas, src->n_tclas);
+ struct cfg80211_scs_desc *dst = pos;
+
+ memcpy(dst, src, len);
+ pos += len;
+
+ if (src->qos_char_len) {
+ memcpy(pos, src->qos_char, src->qos_char_len);
+ dst->qos_char = pos;
+ } else {
+ dst->qos_char = NULL;
+ }
+
+ return PTR_ALIGN(pos + src->qos_char_len, __alignof__(*dst));
+}
+
+static u8 ieee80211_scs_rule_find(struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, u8 id)
+{
+ u8 i;
+
+ for (i = 0; i < n_rules; i++)
+ if (rule[i]->id == id)
+ break;
+
+ return i;
+}
+
+/*
+ * One request is an edit of the rule set, so the result mixes descriptors of
+ * this request with descriptors of earlier ones. @rule therefore holds a mix
+ * of pointers into the old set and pointers the caller owns, and the block
+ * built from it owns a copy of every one of them.
+ */
+static struct ieee80211_scs_sta *
+ieee80211_scs_sta_build(struct cfg80211_scs_desc * const *rule, u8 n_rules)
+{
+ struct ieee80211_scs_sta *scs;
+ size_t head, size;
+ void *pos;
+ u8 i;
+
+ if (!n_rules)
+ return NULL;
+
+ head = ALIGN(struct_size(scs, rule, n_rules), __alignof__(**rule));
+
+ size = head;
+ for (i = 0; i < n_rules; i++)
+ size += ieee80211_scs_rule_size(rule[i]);
+
+ /*
+ * A station may hold 255 rules of 255 classifiers, which is past what
+ * the page allocator hands out in one piece.
+ */
+ scs = kvzalloc(size, GFP_KERNEL);
+ if (!scs)
+ return ERR_PTR(-ENOMEM);
+
+ /* Set before the array is filled, for __counted_by() */
+ scs->n_rules = n_rules;
+
+ pos = (void *)scs + head;
+ for (i = 0; i < n_rules; i++) {
+ scs->rule[i] = pos;
+ pos = ieee80211_scs_rule_copy(rule[i], pos);
+ }
+
+ return scs;
+}
+
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_scs_sta *scs, *old;
+ struct cfg80211_scs_desc **rule;
+ struct sta_info *sta;
+ u8 i, n_rules = 0;
+
+ lockdep_assert_wiphy(wiphy);
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ old = wiphy_dereference(wiphy, sta->scs);
+
+ /*
+ * Every rule carries an SCSID of its own and an SCSID is 1 to 255, so
+ * this bounds the working array, and the resulting set with it.
+ */
+ rule = kcalloc((old ? old->n_rules : 0) + n_desc, sizeof(*rule),
+ GFP_KERNEL);
+ if (!rule)
+ return -ENOMEM;
+
+ if (old) {
+ n_rules = old->n_rules;
+ memcpy(rule, old->rule, n_rules * sizeof(*rule));
+ }
+
+ for (i = 0; i < n_desc; i++) {
+ u8 at = ieee80211_scs_rule_find(rule, n_rules, desc[i]->id);
+
+ if (desc[i]->req_type == NL80211_SCS_REQ_REMOVE) {
+ /* 11.25.2 answers any removal with its own status */
+ res[i].status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+ if (at < n_rules) {
+ n_rules--;
+ memmove(&rule[at], &rule[at + 1],
+ (n_rules - at) * sizeof(*rule));
+ }
+
+ continue;
+ }
+
+ if (at < n_rules)
+ rule[at] = desc[i];
+ else
+ rule[n_rules++] = desc[i];
+ }
+
+ scs = ieee80211_scs_sta_build(rule, n_rules);
+ kfree(rule);
+ if (IS_ERR(scs))
+ return PTR_ERR(scs);
+
+ rcu_assign_pointer(sta->scs, scs);
+ if (old)
+ kvfree_rcu(old, rcu_head);
+
+ return 0;
+}
+
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_mscs_sta *mscs = NULL, *old;
+ struct sta_info *sta;
+ u32 tu;
+
+ lockdep_assert_wiphy(wiphy);
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ if (desc->req_type != NL80211_SCS_REQ_REMOVE) {
+ mscs = kzalloc_obj(*mscs);
+ if (!mscs)
+ return -ENOMEM;
+
+ mscs->layout = desc->fields;
+ mscs->up_bitmap = desc->up_bitmap;
+ mscs->up_limit = desc->up_limit;
+
+ /*
+ * The field is a u32 of TUs, up to about 50 days, which
+ * jiffies arithmetic cannot carry: time_after_eq() needs the
+ * difference to stay well inside the wrap. 11.25.3 only
+ * permits dropping a variable once the timeout has passed and
+ * nowhere requires keeping one until then, so capping is a
+ * choice about usefulness rather than conformance, and an idle
+ * hour leaves nothing worth keeping. A short timeout rounds
+ * down to nothing at a low HZ, and zero would mean never, so
+ * keep a jiffy.
+ */
+ tu = min(desc->stream_timeout, IEEE80211_MSCS_TIMEOUT_MAX_TU);
+ mscs->timeout = max(1UL, usecs_to_jiffies(tu * 1024));
+ }
+
+ old = wiphy_dereference(wiphy, sta->mscs);
+ rcu_assign_pointer(sta->mscs, mscs);
+ if (old)
+ kfree_rcu(old, rcu_head);
+
+ return 0;
+}
+
+void ieee80211_sta_scs_free(struct sta_info *sta)
+{
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_scs_sta *scs;
+
+ scs = rcu_dereference_raw(sta->scs);
+ RCU_INIT_POINTER(sta->scs, NULL);
+ kvfree(scs);
+
+ mscs = rcu_dereference_raw(sta->mscs);
+ RCU_INIT_POINTER(sta->mscs, NULL);
+ kfree(mscs);
+}
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index fdf00cbf49d8..e14d78b002f1 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -508,6 +508,7 @@ void sta_info_free(struct ieee80211_local *local, struct sta_info *sta)
kfree(sta->mesh);
#endif
+ ieee80211_sta_scs_free(sta);
sta_info_free_link(&sta->deflink);
kfree(sta);
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index ee0d32877c5b..2682b3095474 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -635,6 +635,39 @@ struct ieee80211_sta_removed_link_stats {
};
/**
+ * struct ieee80211_scs_sta - the SCS rule set of one station
+ *
+ * @rcu_head: for freeing
+ * @n_rules: number of entries in @rule
+ * @rule: the active descriptors, in the order they were installed
+ *
+ * One allocation holds this structure, the descriptors it points to and their
+ * QoS Characteristics octets, so the whole set has one lifetime.
+ */
+struct ieee80211_scs_sta {
+ struct rcu_head rcu_head;
+ u8 n_rules;
+ struct cfg80211_scs_desc *rule[] __counted_by(n_rules);
+};
+
+/**
+ * struct ieee80211_mscs_sta - the MSCS of one station
+ *
+ * @rcu_head: for freeing
+ * @layout: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority
+ * @timeout: minimum lifetime of a learned value, in jiffies
+ */
+struct ieee80211_mscs_sta {
+ struct rcu_head rcu_head;
+ u32 layout;
+ u8 up_bitmap;
+ u8 up_limit;
+ unsigned long timeout;
+};
+
+/**
* struct sta_info - STA information
*
* This structure collects information about a station that
@@ -694,6 +727,8 @@ struct ieee80211_sta_removed_link_stats {
*
* @fast_tx: TX fastpath information
* @fast_rx: RX fastpath information
+ * @scs: SCS rule set, or %NULL
+ * @mscs: MSCS parameters, or %NULL
* @tdls_chandef: a TDLS peer can have a wider chandef that is compatible to
* the BSS one.
* @frags: fragment cache
@@ -730,6 +765,9 @@ struct sta_info {
struct ieee80211_fast_tx __rcu *fast_tx;
struct ieee80211_fast_rx __rcu *fast_rx;
+ struct ieee80211_scs_sta __rcu *scs;
+ struct ieee80211_mscs_sta __rcu *mscs;
+
#ifdef CONFIG_MAC80211_MESH
struct mesh_sta *mesh;
#endif
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 09/15] wifi: mac80211: classify transmitted MSDUs with SCS
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (7 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 08/15] wifi: mac80211: add SCS and MSCS rule storage Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 10/15] wifi: mac80211: learn MSCS flows on receive Felix Fietkau
` (6 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Hook the evaluator into ieee80211_select_queue(), which every software
transmit path reaches, including the 802.11 encapsulation offload path
and the multicast to unicast copies.
It runs ahead of cfg80211_classify8021d(), which keeps the priority a
frame already carries. A frame that the receive path forwards between two
stations carries one, and 11.25.2 covers a forwarded MSDU as much as a
local one.
This completes SCS.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/mac80211/ieee80211_i.h | 1 +
net/mac80211/scs.c | 31 +++++++++++++++++++++++++++++++
net/mac80211/wme.c | 13 +++++++++++++
3 files changed, 45 insertions(+)
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 31a630e68a2b..ce2167c8a252 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -2211,6 +2211,7 @@ int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
const u8 *peer, struct cfg80211_mscs_desc *desc);
void ieee80211_sta_scs_free(struct sta_info *sta);
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);
/* link handling */
void ieee80211_link_setup(struct ieee80211_link_data *link);
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
index d2daefb8e90c..ea92c0104d92 100644
--- a/net/mac80211/scs.c
+++ b/net/mac80211/scs.c
@@ -93,6 +93,37 @@ ieee80211_scs_sta_build(struct cfg80211_scs_desc * const *rule, u8 n_rules)
return scs;
}
+/**
+ * ieee80211_flow_classify - give an MSDU the user priority of its stream
+ *
+ * @sta: the receiver
+ * @skb: the MSDU, in IEEE 802.3 format
+ *
+ * Return: %true when it set skb->priority, %false to leave the frame to the
+ * QoS map.
+ */
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)
+{
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+ struct ieee80211_scs_sta *scs;
+
+ scs = rcu_dereference(sta->scs);
+ if (!scs)
+ return false;
+
+ if (!cfg80211_flow_parse(skb, &info))
+ return false;
+
+ cfg80211_scs_evaluate(scs->rule, scs->n_rules, &info, &verdict);
+ if (!verdict.match)
+ return false;
+
+ skb->priority = verdict.up;
+
+ return true;
+}
+
int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
const u8 *peer, struct cfg80211_scs_desc * const *desc,
struct cfg80211_scs_result *res, u8 n_desc)
diff --git a/net/mac80211/wme.c b/net/mac80211/wme.c
index 1601be576414..7ed9243f6045 100644
--- a/net/mac80211/wme.c
+++ b/net/mac80211/wme.c
@@ -174,6 +174,19 @@ u16 ieee80211_select_queue(struct ieee80211_sub_if_data *sdata,
goto downgrade;
}
+ /*
+ * Both services classify at the AP, so a non-AP station applies none
+ * of the descriptors it holds for its driver. A frame the receive path
+ * forwards between two stations also reaches
+ * cfg80211_classify8021d() with a priority it must keep, so the
+ * classifiers run ahead of it rather than behind it.
+ */
+ if (sta &&
+ (sdata->vif.type == NL80211_IFTYPE_AP ||
+ sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
+ ieee80211_flow_classify(sta, skb))
+ goto downgrade;
+
/* use the data classifier to determine what 802.1d tag the
* data frame has */
qos_map = rcu_dereference(sdata->qos_map);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 10/15] wifi: mac80211: learn MSCS flows on receive
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (8 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 09/15] wifi: mac80211: classify transmitted MSDUs with SCS Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 11/15] wifi: mac80211: apply MSCS on transmit Felix Fietkau
` (5 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
An AP mirrors the priority a station gives its own uplink onto the
downlink of the same flow, so the receive path records the flow UP from
individually addressed frames of the station whose user priority the
request named.
The key is built for the reverse direction, so the transmit path finds it
without a second layout. Entries live in one hash table per BSS, keyed by
the peer as well as the tuple, and a per station list carries what
teardown and the collector walk.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/mac80211/ieee80211_i.h | 7 +-
net/mac80211/iface.c | 12 +-
net/mac80211/rx.c | 5 +-
net/mac80211/scs.c | 286 +++++++++++++++++++++++++++++++++++++-
net/mac80211/sta_info.h | 40 +++++-
5 files changed, 348 insertions(+), 2 deletions(-)
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index ce2167c8a252..b17fa83f2003 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -336,6 +336,10 @@ struct ieee80211_if_ap {
bool multicast_to_unicast;
bool active;
+
+ /* learned MSCS entries of every station of this BSS, and its VLANs */
+ struct rhashtable flow_tbl;
+ struct wiphy_delayed_work flow_gc_work;
};
struct ieee80211_if_vlan {
@@ -2212,6 +2216,9 @@ int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
const u8 *peer, struct cfg80211_mscs_desc *desc);
void ieee80211_sta_scs_free(struct sta_info *sta);
bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx);
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata);
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata);
/* link handling */
void ieee80211_link_setup(struct ieee80211_link_data *link);
diff --git a/net/mac80211/iface.c b/net/mac80211/iface.c
index 43460a705a6b..593e93f71c20 100644
--- a/net/mac80211/iface.c
+++ b/net/mac80211/iface.c
@@ -542,6 +542,9 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
atomic_dec(&local->iff_allmultis);
if (sdata->vif.type == NL80211_IFTYPE_AP) {
+ /* every station is gone, so nothing can learn any more */
+ ieee80211_flow_tbl_destroy(sdata);
+
local->fif_pspoll--;
local->fif_probe_req--;
} else if (sdata->vif.type == NL80211_IFTYPE_ADHOC) {
@@ -1405,6 +1408,10 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
break;
}
case NL80211_IFTYPE_AP:
+ res = ieee80211_flow_tbl_init(sdata);
+ if (res)
+ return res;
+ break;
case NL80211_IFTYPE_MESH_POINT:
case NL80211_IFTYPE_STATION:
case NL80211_IFTYPE_MONITOR:
@@ -1439,7 +1446,7 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
* no need to worry about AP_VLAN/NAN_DATA cleanup since
* in that case we can't have open_count == 0
*/
- return res;
+ goto err_flow_tbl;
}
ieee80211_led_radio(local, true);
ieee80211_mod_tpt_led_trig(local,
@@ -1611,6 +1618,9 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
list_del(&sdata->u.vlan.list);
/* Might not be initialized yet, but it is harmless */
sdata->bss = NULL;
+ err_flow_tbl:
+ if (sdata->vif.type == NL80211_IFTYPE_AP)
+ ieee80211_flow_tbl_destroy(sdata);
return res;
}
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index 5e26be8e27d8..bf249e66e069 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -2815,6 +2815,9 @@ ieee80211_deliver_skb(struct ieee80211_rx_data *rx)
u64_stats_update_end(&rx->link_sta->rx_stats.syncp);
}
+ if (rx->sta)
+ ieee80211_flow_learn(rx);
+
if ((sdata->vif.type == NL80211_IFTYPE_AP ||
sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
!(sdata->flags & IEEE80211_SDATA_DONT_BRIDGE_PACKETS) &&
@@ -4980,6 +4983,8 @@ static void ieee80211_rx_8023(struct ieee80211_rx_data *rx,
u64_stats_add(&stats->bytes, orig_len);
u64_stats_update_end(&stats->syncp);
+ ieee80211_flow_learn(rx);
+
if (fast_rx->internal_forward) {
struct sk_buff *xmit_skb = NULL;
if (is_multicast_ether_addr(da)) {
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
index ea92c0104d92..12730e836962 100644
--- a/net/mac80211/scs.c
+++ b/net/mac80211/scs.c
@@ -5,6 +5,7 @@
* Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
*/
#include <linux/ieee80211.h>
+#include <linux/module.h>
#include <net/cfg80211.h>
#include "ieee80211_i.h"
#include "sta_info.h"
@@ -12,6 +13,27 @@
/* An hour, in TUs of 1024 microseconds */
#define IEEE80211_MSCS_TIMEOUT_MAX_TU 3515625
+/*
+ * Only the station itself can fill its own table, because every entry comes
+ * from one of its uplink frames, so the bound is per station and needs no
+ * admission rule. See bss_entries_limit in net/wireless/scan.c for the same
+ * shape of knob.
+ */
+static u32 mscs_entries_limit = 64;
+module_param(mscs_entries_limit, uint, 0644);
+MODULE_PARM_DESC(mscs_entries_limit, "limit on learned MSCS flows per station");
+
+/* The hash covers the peer and the key, and not the padding behind them */
+static_assert(offsetofend(struct ieee80211_flow_hkey, key) ==
+ sizeof(struct sta_info *) + sizeof(struct cfg80211_flow_key));
+
+static const struct rhashtable_params flow_rht_params = {
+ .head_offset = offsetof(struct ieee80211_flow_entry, node),
+ .key_offset = offsetof(struct ieee80211_flow_entry, hkey),
+ .key_len = offsetofend(struct ieee80211_flow_hkey, key),
+ .automatic_shrinking = true,
+};
+
static size_t ieee80211_scs_rule_size(const struct cfg80211_scs_desc *desc)
{
return ALIGN(struct_size(desc, tclas, desc->n_tclas) +
@@ -124,6 +146,240 @@ bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)
return true;
}
+static void ieee80211_flow_entry_free(struct ieee80211_if_ap *ap,
+ struct ieee80211_flow_entry *entry)
+{
+ rhashtable_remove_fast(&ap->flow_tbl, &entry->node, flow_rht_params);
+ list_del(&entry->list);
+ kfree_rcu(entry, rcu_head);
+}
+
+/* Drop everything one station learned, with @mscs->lock held if it can race */
+static void ieee80211_flow_purge(struct ieee80211_if_ap *ap,
+ struct ieee80211_mscs_sta *mscs)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list)
+ ieee80211_flow_entry_free(ap, entry);
+
+ mscs->n_entries = 0;
+}
+
+/*
+ * 9.4.2.242 calls the Stream Timeout a minimum for keeping a variable, so an
+ * entry is dropped once it has been idle for longer than that. Each run
+ * reports how long the earliest survivor has left, and the work sleeps for
+ * that; with nothing left it stops until a station learns again.
+ */
+static unsigned long ieee80211_flow_gc_sta(struct ieee80211_if_ap *ap,
+ struct sta_info *sta)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+ struct ieee80211_mscs_sta *mscs;
+ unsigned long delay = 0;
+
+ mscs = wiphy_dereference(sta->local->hw.wiphy, sta->mscs);
+ if (!mscs)
+ return 0;
+
+ spin_lock_bh(&mscs->lock);
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list) {
+ unsigned long dead = READ_ONCE(entry->last_update) +
+ mscs->timeout;
+
+ if (time_after_eq(jiffies, dead)) {
+ ieee80211_flow_entry_free(ap, entry);
+ mscs->n_entries--;
+ continue;
+ }
+
+ /* A survivor has at least one jiffy left, so zero means none */
+ if (!delay || time_before(dead, jiffies + delay))
+ delay = dead - jiffies;
+ }
+
+ spin_unlock_bh(&mscs->lock);
+
+ return delay;
+}
+
+static void ieee80211_flow_gc_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+ struct ieee80211_sub_if_data *sdata;
+ struct ieee80211_if_ap *ap;
+ unsigned long delay = 0;
+ struct sta_info *sta;
+
+ ap = container_of(work, struct ieee80211_if_ap, flow_gc_work.work);
+ sdata = container_of(ap, struct ieee80211_sub_if_data, u.ap);
+
+ list_for_each_entry(sta, &sdata->local->sta_list, list) {
+ unsigned long sta_delay;
+
+ if (sta->sdata->bss != ap)
+ continue;
+
+ sta_delay = ieee80211_flow_gc_sta(ap, sta);
+ if (sta_delay && (!delay || sta_delay < delay))
+ delay = sta_delay;
+ }
+
+ if (delay)
+ wiphy_delayed_work_queue(wiphy, &ap->flow_gc_work, delay);
+}
+
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_init(&sdata->u.ap.flow_gc_work,
+ ieee80211_flow_gc_work);
+
+ return rhashtable_init(&sdata->u.ap.flow_tbl, &flow_rht_params);
+}
+
+static void ieee80211_flow_tbl_free(void *ptr, void *arg)
+{
+ struct ieee80211_flow_entry *entry = ptr;
+
+ kfree(entry);
+}
+
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_cancel(sdata->local->hw.wiphy,
+ &sdata->u.ap.flow_gc_work);
+ rhashtable_free_and_destroy(&sdata->u.ap.flow_tbl,
+ ieee80211_flow_tbl_free, NULL);
+}
+
+static void ieee80211_flow_insert(struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_mscs_sta *mscs,
+ const struct ieee80211_flow_hkey *hkey, u8 up)
+{
+ struct ieee80211_flow_entry *entry, *other;
+ bool first;
+
+ entry = kzalloc_obj(*entry, GFP_ATOMIC);
+ if (!entry)
+ return;
+
+ entry->hkey.sta = hkey->sta;
+ entry->hkey.key = hkey->key;
+ entry->up = up;
+ entry->last_update = jiffies;
+
+ spin_lock_bh(&mscs->lock);
+
+ /*
+ * The station may have been given another MSCS since this one was
+ * read, and that one took this list apart under the same lock.
+ */
+ if (rcu_dereference(hkey->sta->mscs) != mscs)
+ goto drop;
+
+ if (mscs->n_entries >= mscs_entries_limit)
+ goto drop;
+
+ other = rhashtable_lookup_get_insert_fast(&sdata->bss->flow_tbl,
+ &entry->node,
+ flow_rht_params);
+ if (other) {
+ /*
+ * Another CPU learned the same tuple first, which a coarse
+ * mask makes ordinary. 11.25.3 item c) 3) discards the
+ * previous value, so either writer may win. A resize gives an
+ * error instead, and the next frame of the flow learns again.
+ */
+ if (!IS_ERR(other))
+ WRITE_ONCE(other->up, up);
+
+ goto drop;
+ }
+
+ list_add_tail(&entry->list, &mscs->entries);
+ first = mscs->n_entries++ == 0;
+ spin_unlock_bh(&mscs->lock);
+
+ /*
+ * The collector reschedules itself while any entry is left, so it only
+ * has to be woken when a station fills an empty list. Waking it at once
+ * rather than at this station's deadline is deliberate: the queue is a
+ * mod_timer(), so a later deadline would displace an earlier one.
+ */
+ if (first)
+ wiphy_delayed_work_queue(sdata->local->hw.wiphy,
+ &sdata->bss->flow_gc_work, 1);
+
+ return;
+
+drop:
+ spin_unlock_bh(&mscs->lock);
+ kfree(entry);
+}
+
+/**
+ * ieee80211_flow_learn - record the user priority of an uplink flow
+ *
+ * @rx: the received frame, in IEEE 802.3 format
+ *
+ * The AP mirrors the priority a station gives its own uplink onto the
+ * downlink of the same flow, so the key is built for the reverse direction
+ * and the transmit path finds it without a second layout.
+ */
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx)
+{
+ struct ieee80211_sub_if_data *sdata = rx->sdata;
+ const struct ethhdr *eth = (void *)rx->skb->data;
+ struct ieee80211_flow_hkey hkey;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_entry *entry;
+ struct cfg80211_flow_info info;
+ /*
+ * The TID that ieee80211_parse_qos() recorded, or for a frame the
+ * device decapped, whatever the driver put there. The forwarding path
+ * a few lines below the caller reads the same field.
+ */
+ u32 up = rx->skb->priority;
+
+ if (sdata->vif.type != NL80211_IFTYPE_AP &&
+ sdata->vif.type != NL80211_IFTYPE_AP_VLAN)
+ return;
+
+ mscs = rcu_dereference(rx->sta->mscs);
+ if (!mscs)
+ return;
+
+ /*
+ * 11.25.3 item c) learns from an individually addressed MSDU that the
+ * station sent itself, and only for a user priority its request named.
+ */
+ if (up > 7 || !(mscs->up_bitmap & BIT(up)))
+ return;
+
+ if (is_multicast_ether_addr(eth->h_dest) ||
+ !ether_addr_equal(eth->h_source, rx->sta->addr))
+ return;
+
+ if (!cfg80211_flow_parse(rx->skb, &info))
+ return;
+
+ hkey.sta = rx->sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout,
+ CFG80211_FLOW_MIRRORED, &hkey.key))
+ return;
+
+ entry = rhashtable_lookup(&sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (entry) {
+ WRITE_ONCE(entry->up, up);
+ WRITE_ONCE(entry->last_update, jiffies);
+ return;
+ }
+
+ ieee80211_flow_insert(sdata, mscs, &hkey, up);
+}
+
int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
const u8 *peer, struct cfg80211_scs_desc * const *desc,
struct cfg80211_scs_result *res, u8 n_desc)
@@ -200,6 +456,10 @@ int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
lockdep_assert_wiphy(wiphy);
+ /* 11.25.3 makes MSCS a service an AP provides, so it needs a BSS */
+ if (!sdata->bss)
+ return -EOPNOTSUPP;
+
sta = sta_info_get_bss(sdata, peer);
if (!sta)
return -ENOENT;
@@ -226,12 +486,28 @@ int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
*/
tu = min(desc->stream_timeout, IEEE80211_MSCS_TIMEOUT_MAX_TU);
mscs->timeout = max(1UL, usecs_to_jiffies(tu * 1024));
+
+ spin_lock_init(&mscs->lock);
+ INIT_LIST_HEAD(&mscs->entries);
}
old = wiphy_dereference(wiphy, sta->mscs);
rcu_assign_pointer(sta->mscs, mscs);
- if (old)
+
+ if (old) {
+ /*
+ * The keys of the learned entries were built with the layout
+ * that is going away, and 11.25.3 deletes the list of
+ * UP{tuple} variables on a teardown, so they go either way. A
+ * receive path that still holds the old MSCS takes the same
+ * lock, sees that the station has moved on and adds nothing.
+ */
+ spin_lock_bh(&old->lock);
+ ieee80211_flow_purge(sta->sdata->bss, old);
+ spin_unlock_bh(&old->lock);
+
kfree_rcu(old, rcu_head);
+ }
return 0;
}
@@ -247,5 +523,13 @@ void ieee80211_sta_scs_free(struct sta_info *sta)
mscs = rcu_dereference_raw(sta->mscs);
RCU_INIT_POINTER(sta->mscs, NULL);
+ if (!mscs)
+ return;
+
+ /*
+ * sta_info_free() runs a grace period after the station left the hash,
+ * so nothing can reach this list any more and it needs no lock.
+ */
+ ieee80211_flow_purge(sta->sdata->bss, mscs);
kfree(mscs);
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 2682b3095474..7c8978c142d4 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -658,6 +658,9 @@ struct ieee80211_scs_sta {
* @up_bitmap: user priorities that the AP learns from, one bit each
* @up_limit: ceiling for the assigned user priority
* @timeout: minimum lifetime of a learned value, in jiffies
+ * @lock: protects @entries and @n_entries
+ * @entries: the learned entries of this station, for teardown
+ * @n_entries: number of entries in @entries
*/
struct ieee80211_mscs_sta {
struct rcu_head rcu_head;
@@ -665,6 +668,43 @@ struct ieee80211_mscs_sta {
u8 up_bitmap;
u8 up_limit;
unsigned long timeout;
+
+ spinlock_t lock; /* protects the entry list and its count */
+ struct list_head entries;
+ u32 n_entries;
+};
+
+/**
+ * struct ieee80211_flow_hkey - what a learned entry is looked up by
+ *
+ * @sta: the peer, so that a flow of one station never answers for another
+ * @key: the masked tuple, in the downlink direction
+ *
+ * Not packed: the whole of it is compared, but a packed aggregate would force
+ * byte-wise access to @sta.
+ */
+struct ieee80211_flow_hkey {
+ struct sta_info *sta;
+ struct cfg80211_flow_key key;
+};
+
+/**
+ * struct ieee80211_flow_entry - one learned UP{tuple} variable
+ *
+ * @hkey: what it is looked up by, keep first
+ * @node: hash table linkage
+ * @list: linkage in &ieee80211_mscs_sta.entries
+ * @last_update: jiffies of the last uplink frame of this flow
+ * @up: the learned user priority
+ * @rcu_head: for freeing
+ */
+struct ieee80211_flow_entry {
+ struct ieee80211_flow_hkey hkey;
+ struct rhash_head node;
+ struct list_head list;
+ unsigned long last_update;
+ u8 up;
+ struct rcu_head rcu_head;
};
/**
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 11/15] wifi: mac80211: apply MSCS on transmit
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (9 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 10/15] wifi: mac80211: learn MSCS flows on receive Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 12/15] wifi: mac80211: add the sta_set_scs driver op Felix Fietkau
` (4 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Look up the learned flow UP for an MSDU that no SCS descriptor claimed,
which is the order 11.25.3 item d) 2) asks for, and cap the result with
the User Priority Limit of the request.
This completes MSCS.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/mac80211/scs.c | 32 ++++++++++++++++++++++++++++----
1 file changed, 28 insertions(+), 4 deletions(-)
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
index 12730e836962..439bbb2df923 100644
--- a/net/mac80211/scs.c
+++ b/net/mac80211/scs.c
@@ -126,22 +126,46 @@ ieee80211_scs_sta_build(struct cfg80211_scs_desc * const *rule, u8 n_rules)
*/
bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)
{
+ struct ieee80211_flow_entry *entry;
struct cfg80211_scs_verdict verdict;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_hkey hkey;
struct cfg80211_flow_info info;
struct ieee80211_scs_sta *scs;
scs = rcu_dereference(sta->scs);
- if (!scs)
+ mscs = rcu_dereference(sta->mscs);
+ if (!scs && !mscs)
return false;
if (!cfg80211_flow_parse(skb, &info))
return false;
- cfg80211_scs_evaluate(scs->rule, scs->n_rules, &info, &verdict);
- if (!verdict.match)
+ if (scs) {
+ cfg80211_scs_evaluate(scs->rule, scs->n_rules, &info,
+ &verdict);
+ if (verdict.match) {
+ skb->priority = verdict.up;
+
+ return true;
+ }
+ }
+
+ /* 11.25.3 item d) 2) leaves an MSDU of an SCS stream to that stream */
+ if (!mscs)
+ return false;
+
+ hkey.sta = sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout, CFG80211_FLOW_AS_IS,
+ &hkey.key))
+ return false;
+
+ entry = rhashtable_lookup(&sta->sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (!entry)
return false;
- skb->priority = verdict.up;
+ skb->priority = min_t(u8, READ_ONCE(entry->up), mscs->up_limit);
return true;
}
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 12/15] wifi: mac80211: add the sta_set_scs driver op
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (10 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 11/15] wifi: mac80211: apply MSCS on transmit Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 13/15] wifi: mac80211: add SCS and MSCS debugfs files Felix Fietkau
` (3 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Hand the SCS descriptors of one request to the driver and let it answer
each of them. This is the only path by which the QoS Characteristics
element reaches a driver, which is its only consumer.
A driver declines a descriptor by writing a status into the result entry
of the same index, and mac80211 leaves that descriptor out of the set.
Only a driver can schedule a stream to what a traffic description asks
for, so accepting one it cannot serve would promise a station a guarantee
that nothing provides. 11.25.2 asks per SCSID, so the answer is per
descriptor too.
The reverse does not hold: classification runs in software whichever way
a driver answers, so a driver must not decline a descriptor merely
because it cannot offload the classifier.
Without the op, mac80211 declines every descriptor that carries a traffic
description for the same reason.
The driver answers before the set is published, and a declined change
keeps the descriptor it had, which 11.25.2 requires.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
include/net/mac80211.h | 22 ++++++-
net/mac80211/driver-ops.h | 26 +++++++-
net/mac80211/ieee80211_i.h | 1 +-
net/mac80211/scs.c | 142 +++++++++++++++++++++++++++++---------
net/mac80211/trace.h | 27 +++++++-
net/mac80211/util.c | 2 +-
6 files changed, 189 insertions(+), 31 deletions(-)
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index 9d1fac6e8082..d9e4505ecb47 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -5083,6 +5083,28 @@ struct ieee80211_ops {
struct cfg80211_pmsr_request *request);
void (*abort_pmsr)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct cfg80211_pmsr_request *request);
+ /**
+ * @sta_set_scs:
+ * Called with the SCS descriptors one station requests, to answer each
+ * in the result entry of the same index. Optional, and reached only
+ * when the wiphy reports %NL80211_EXT_FEATURE_SCS; without it every
+ * descriptor carrying a QoS Characteristics element is declined.
+ *
+ * - Results arrive zeroed, which is acceptance. Write only refusals.
+ * - A negative return fails the whole request.
+ * - Decline a traffic description that cannot be served, nothing else.
+ * A decline drops the descriptor whole, classifier included.
+ * - A declined change keeps the previous descriptor of that SCSID.
+ * - A removal is the cue to drop driver state. mac80211 answers it
+ * whatever the driver writes.
+ * - A descriptor and its qos_char octets last only for the call.
+ *
+ * This callback may sleep.
+ */
+ int (*sta_set_scs)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
int (*set_tid_config)(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
diff --git a/net/mac80211/driver-ops.h b/net/mac80211/driver-ops.h
index f1c0b87fddd5..f7684e8db025 100644
--- a/net/mac80211/driver-ops.h
+++ b/net/mac80211/driver-ops.h
@@ -1552,6 +1552,32 @@ static inline void drv_del_nan_func(struct ieee80211_local *local,
trace_drv_return_void(local);
}
+static inline int drv_sta_set_scs(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret;
+
+ might_sleep();
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ if (!local->ops->sta_set_scs || !sta->uploaded)
+ return 0;
+
+ sdata = get_bss_sdata(sdata);
+ if (!check_sdata_in_driver(sdata))
+ return -EIO;
+
+ trace_drv_sta_set_scs(local, sdata, &sta->sta, n_desc);
+ ret = local->ops->sta_set_scs(&local->hw, &sdata->vif, &sta->sta, desc,
+ res, n_desc);
+ trace_drv_return_int(local, ret);
+
+ return ret;
+}
+
static inline int drv_set_tid_config(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
struct ieee80211_sta *sta,
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index b17fa83f2003..5990ea261a42 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -2214,6 +2214,7 @@ int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
struct cfg80211_scs_result *res, u8 n_desc);
int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
const u8 *peer, struct cfg80211_mscs_desc *desc);
+void ieee80211_sta_scs_reconfig(struct sta_info *sta);
void ieee80211_sta_scs_free(struct sta_info *sta);
bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);
void ieee80211_flow_learn(struct ieee80211_rx_data *rx);
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
index 439bbb2df923..794ce5c9a4c0 100644
--- a/net/mac80211/scs.c
+++ b/net/mac80211/scs.c
@@ -7,6 +7,7 @@
#include <linux/ieee80211.h>
#include <linux/module.h>
#include <net/cfg80211.h>
+#include "driver-ops.h"
#include "ieee80211_i.h"
#include "sta_info.h"
@@ -48,6 +49,8 @@ static void *ieee80211_scs_rule_copy(const struct cfg80211_scs_desc *src,
struct cfg80211_scs_desc *dst = pos;
memcpy(dst, src, len);
+ /* A stored rule is in force, so handing it back is an install */
+ dst->req_type = NL80211_SCS_REQ_ADD;
pos += len;
if (src->qos_char_len) {
@@ -72,47 +75,57 @@ static u8 ieee80211_scs_rule_find(struct cfg80211_scs_desc * const *rule,
return i;
}
+static size_t ieee80211_scs_sta_head(unsigned int n_rules)
+{
+ struct ieee80211_scs_sta *scs;
+
+ return ALIGN(struct_size(scs, rule, n_rules),
+ __alignof__(struct cfg80211_scs_desc));
+}
+
/*
- * One request is an edit of the rule set, so the result mixes descriptors of
- * this request with descriptors of earlier ones. @rule therefore holds a mix
- * of pointers into the old set and pointers the caller owns, and the block
- * built from it owns a copy of every one of them.
+ * Every rule that could end up in the set, counted before the driver is asked
+ * so that no allocation can fail once it has programmed what it accepted. A
+ * descriptor and the rule it would replace are both counted, because which of
+ * the two survives is the answer that has not been given yet.
*/
-static struct ieee80211_scs_sta *
-ieee80211_scs_sta_build(struct cfg80211_scs_desc * const *rule, u8 n_rules)
+static size_t ieee80211_scs_sta_size(const struct ieee80211_scs_sta *old,
+ struct cfg80211_scs_desc * const *desc,
+ u8 n_desc, unsigned int n_alloc)
{
- struct ieee80211_scs_sta *scs;
- size_t head, size;
- void *pos;
- u8 i;
+ size_t size = ieee80211_scs_sta_head(n_alloc);
+ unsigned int i;
- if (!n_rules)
- return NULL;
+ for (i = 0; old && i < old->n_rules; i++)
+ size += ieee80211_scs_rule_size(old->rule[i]);
- head = ALIGN(struct_size(scs, rule, n_rules), __alignof__(**rule));
+ for (i = 0; i < n_desc; i++)
+ size += ieee80211_scs_rule_size(desc[i]);
- size = head;
- for (i = 0; i < n_rules; i++)
- size += ieee80211_scs_rule_size(rule[i]);
+ return size;
+}
- /*
- * A station may hold 255 rules of 255 classifiers, which is past what
- * the page allocator hands out in one piece.
- */
- scs = kvzalloc(size, GFP_KERNEL);
- if (!scs)
- return ERR_PTR(-ENOMEM);
+/*
+ * One request is an edit of the rule set, so the result mixes descriptors of
+ * this request with descriptors of earlier ones. @rule therefore holds a mix
+ * of pointers into the old set and pointers the caller owns, and the block
+ * built from it owns a copy of every one of them. It was sized for @n_alloc
+ * of them, which is what places the first copy.
+ */
+static void ieee80211_scs_sta_fill(struct ieee80211_scs_sta *scs,
+ struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, unsigned int n_alloc)
+{
+ void *pos = (void *)scs + ieee80211_scs_sta_head(n_alloc);
+ u8 i;
/* Set before the array is filled, for __counted_by() */
scs->n_rules = n_rules;
- pos = (void *)scs + head;
for (i = 0; i < n_rules; i++) {
scs->rule[i] = pos;
pos = ieee80211_scs_rule_copy(rule[i], pos);
}
-
- return scs;
}
/**
@@ -412,7 +425,9 @@ int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
struct ieee80211_scs_sta *scs, *old;
struct cfg80211_scs_desc **rule;
struct sta_info *sta;
+ unsigned int n_alloc;
u8 i, n_rules = 0;
+ int ret;
lockdep_assert_wiphy(wiphy);
@@ -426,11 +441,32 @@ int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
* Every rule carries an SCSID of its own and an SCSID is 1 to 255, so
* this bounds the working array, and the resulting set with it.
*/
- rule = kcalloc((old ? old->n_rules : 0) + n_desc, sizeof(*rule),
- GFP_KERNEL);
+ n_alloc = (old ? old->n_rules : 0) + n_desc;
+
+ rule = kcalloc(n_alloc, sizeof(*rule), GFP_KERNEL);
if (!rule)
return -ENOMEM;
+ scs = kvzalloc(ieee80211_scs_sta_size(old, desc, n_desc, n_alloc),
+ GFP_KERNEL);
+ if (!scs) {
+ ret = -ENOMEM;
+ goto free;
+ }
+
+ /* Only what the driver takes is installed, so it answers first */
+ if (sdata->local->ops->sta_set_scs && sta->uploaded) {
+ ret = drv_sta_set_scs(sdata->local, sdata, sta, desc, res,
+ n_desc);
+ if (ret)
+ goto free;
+ } else {
+ /* Only a driver reads a traffic description, so none is served */
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
if (old) {
n_rules = old->n_rules;
memcpy(rule, old->rule, n_rules * sizeof(*rule));
@@ -452,22 +488,36 @@ int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
continue;
}
+ /* 11.25.2 leaves a declined change with its earlier rule */
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
if (at < n_rules)
rule[at] = desc[i];
else
rule[n_rules++] = desc[i];
}
- scs = ieee80211_scs_sta_build(rule, n_rules);
+ if (n_rules) {
+ ieee80211_scs_sta_fill(scs, rule, n_rules, n_alloc);
+ } else {
+ kvfree(scs);
+ scs = NULL;
+ }
+
kfree(rule);
- if (IS_ERR(scs))
- return PTR_ERR(scs);
rcu_assign_pointer(sta->scs, scs);
if (old)
kvfree_rcu(old, rcu_head);
return 0;
+
+free:
+ kvfree(scs);
+ kfree(rule);
+
+ return ret;
}
int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
@@ -536,6 +586,36 @@ int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
return 0;
}
+/*
+ * A device that was restarted holds none of the streams it was serving, and
+ * 11.25.2 gives an AP no way to tell a station that one of its streams is
+ * gone, so the set goes down again. A refusal changes nothing at this point:
+ * the rule stays and mac80211 keeps classifying for it.
+ */
+void ieee80211_sta_scs_reconfig(struct sta_info *sta)
+{
+ struct ieee80211_local *local = sta->local;
+ struct ieee80211_scs_sta *scs;
+ u8 i;
+
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ scs = wiphy_dereference(local->hw.wiphy, sta->scs);
+ if (!scs)
+ return;
+
+ /*
+ * One rule per call, so that the result the op insists on fits a local
+ * and nothing here can fail. A path that cannot report a failure is
+ * better off unable to have one.
+ */
+ for (i = 0; i < scs->n_rules; i++) {
+ struct cfg80211_scs_result res = {};
+
+ drv_sta_set_scs(local, sta->sdata, sta, &scs->rule[i], &res, 1);
+ }
+}
+
void ieee80211_sta_scs_free(struct sta_info *sta)
{
struct ieee80211_mscs_sta *mscs;
diff --git a/net/mac80211/trace.h b/net/mac80211/trace.h
index 562a4964afa3..1e6026306fb2 100644
--- a/net/mac80211/trace.h
+++ b/net/mac80211/trace.h
@@ -1023,6 +1023,33 @@ DECLARE_EVENT_CLASS(sta_event,
)
);
+TRACE_EVENT(drv_sta_set_scs,
+ TP_PROTO(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_sta *sta, u8 n_desc),
+
+ TP_ARGS(local, sdata, sta, n_desc),
+
+ TP_STRUCT__entry(
+ LOCAL_ENTRY
+ VIF_ENTRY
+ STA_ENTRY
+ __field(u8, n_desc)
+ ),
+
+ TP_fast_assign(
+ LOCAL_ASSIGN;
+ VIF_ASSIGN;
+ STA_ASSIGN;
+ __entry->n_desc = n_desc;
+ ),
+
+ TP_printk(
+ LOCAL_PR_FMT VIF_PR_FMT STA_PR_FMT " %d descriptors",
+ LOCAL_PR_ARG, VIF_PR_ARG, STA_PR_ARG, __entry->n_desc
+ )
+);
+
DEFINE_EVENT(sta_event, drv_sta_statistics,
TP_PROTO(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index a96078a6bfa2..7f347a3c6429 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -1744,6 +1744,8 @@ static void ieee80211_reconfig_stations(struct ieee80211_sub_if_data *sdata)
state < sta->sta_state; state++)
WARN_ON(drv_sta_state(local, sta->sdata, sta, state,
state + 1));
+
+ ieee80211_sta_scs_reconfig(sta);
}
}
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 13/15] wifi: mac80211: add SCS and MSCS debugfs files
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (11 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 12/15] wifi: mac80211: add the sta_set_scs driver op Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 14/15] wifi: mac80211_hwsim: support SCS and MSCS Felix Fietkau
` (2 subsequent siblings)
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Dump the parsed classifier of every rule and every learned tuple, rather
than counters, because what an operator needs to see is which rule
claimed a flow and what the AP learned from the station.
Both are seq_file iterators, so neither sizes a buffer for a set that the
standard bounds at 255 rules of 255 classifiers. A rule set is one
allocation that a request replaces whole, so a rule is named by its
position and looked up again on every read, and the dump is a snapshot
either way. The learned entries are not an RCU list, so that walk holds
the lock of the station.
Neither takes the wiphy lock, which a debugfs reader must not, because
the remover holds it while it waits for readers to finish.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
net/mac80211/debugfs_sta.c | 238 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 238 insertions(+)
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index ef75255d47d5..adf403043164 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -10,6 +10,7 @@
#include <linux/debugfs.h>
#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
#include "ieee80211_i.h"
#include "debugfs.h"
#include "debugfs_sta.h"
@@ -131,6 +132,241 @@ static ssize_t sta_last_seq_ctrl_read(struct file *file, char __user *userbuf,
}
STA_OPS(last_seq_ctrl);
+static void sta_flow_key_show(struct seq_file *s,
+ const struct cfg80211_flow_key *key, u32 fields)
+{
+ u16 tci = be16_to_cpu(key->vlan_tci);
+
+ if (fields & BIT(CFG80211_FLOW_F_ETH_SA))
+ seq_printf(s, " sa=%pM", key->sa);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_DA))
+ seq_printf(s, " da=%pM", key->da);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_TYPE))
+ seq_printf(s, " ethertype=0x%04x", be16_to_cpu(key->eth_type));
+
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_PCP))
+ seq_printf(s, " pcp=%u",
+ (tci & VLAN_PRIO_MASK) >> VLAN_PRIO_SHIFT);
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_DEI))
+ seq_printf(s, " dei=%u", !!(tci & VLAN_CFI_MASK));
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_VID))
+ seq_printf(s, " vid=%u", tci & VLAN_VID_MASK);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_VERSION))
+ seq_printf(s, " ipv%u", key->ip_version);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_SRC))
+ seq_printf(s, key->ip_version == 6 ? " src=%pI6" : " src=%pI4",
+ &key->src);
+ if (fields & BIT(CFG80211_FLOW_F_IP_DST))
+ seq_printf(s, key->ip_version == 6 ? " dst=%pI6" : " dst=%pI4",
+ &key->dst);
+
+ if (fields & BIT(CFG80211_FLOW_F_SRC_PORT))
+ seq_printf(s, " sport=%u", be16_to_cpu(key->src_port));
+ if (fields & BIT(CFG80211_FLOW_F_DST_PORT))
+ seq_printf(s, " dport=%u", be16_to_cpu(key->dst_port));
+
+ if (fields & BIT(CFG80211_FLOW_F_DSCP))
+ seq_printf(s, " dscp=%u", key->dscp);
+ if (fields & BIT(CFG80211_FLOW_F_PROTO))
+ seq_printf(s, " proto=%u", key->proto);
+ if (fields & BIT(CFG80211_FLOW_F_FLOW_LABEL))
+ seq_printf(s, " flowlabel=0x%05x", be32_to_cpu(key->flow_label));
+}
+
+/*
+ * A rule set is one allocation that a request replaces whole, so a rule is
+ * named by its position and looked up again for every read. One that arrives
+ * while the dump runs may be missed, and the dump is a snapshot either way.
+ */
+static void *sta_scs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs;
+
+ rcu_read_lock();
+ scs = rcu_dereference(sta->scs);
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void *sta_scs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs = rcu_dereference(sta->scs);
+
+ ++*pos;
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void sta_scs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ rcu_read_unlock();
+}
+
+static int sta_scs_seq_show(struct seq_file *s, void *v)
+{
+ static const char * const processing[] = {
+ [CFG80211_TCLAS_PROCESSING_ALL] = "all",
+ [CFG80211_TCLAS_PROCESSING_ANY] = "any",
+ [CFG80211_TCLAS_PROCESSING_DEFAULT] = "default",
+ };
+ const struct cfg80211_scs_desc *desc = v;
+ const char *proc = "none";
+ u8 i;
+
+ if (desc->tclas_processing < ARRAY_SIZE(processing))
+ proc = processing[desc->tclas_processing];
+
+ seq_printf(s, "scsid %u up %u processing %s", desc->id, desc->up, proc);
+
+ if (desc->qos_char)
+ seq_printf(s, " qos_char %u direction %u", desc->qos_char_len,
+ ieee80211_qos_char_direction(desc->qos_char));
+
+ seq_putc(s, '\n');
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ const struct cfg80211_tclas *t = &desc->tclas[i];
+
+ seq_printf(s, " type %u", t->type);
+ sta_flow_key_show(s, &t->key, t->fields);
+ seq_putc(s, '\n');
+ }
+
+ return 0;
+}
+
+static const struct seq_operations sta_scs_seq_ops = {
+ .start = sta_scs_seq_start,
+ .next = sta_scs_seq_next,
+ .stop = sta_scs_seq_stop,
+ .show = sta_scs_seq_show,
+};
+
+static int sta_scs_open(struct inode *inode, struct file *file)
+{
+ int ret = seq_open(file, &sta_scs_seq_ops);
+
+ if (!ret)
+ ((struct seq_file *)file->private_data)->private = inode->i_private;
+
+ return ret;
+}
+
+static const struct file_operations sta_scs_ops = {
+ .open = sta_scs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release,
+};
+
+struct sta_mscs_iter {
+ struct sta_info *sta;
+ struct ieee80211_mscs_sta *mscs;
+};
+
+/* The entry list is not an RCU list, so the whole walk holds the lock */
+static void *sta_mscs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ rcu_read_lock();
+ iter->mscs = rcu_dereference(iter->sta->mscs);
+ if (!iter->mscs)
+ return NULL;
+
+ spin_lock_bh(&iter->mscs->lock);
+
+ if (*pos == 0)
+ return SEQ_START_TOKEN;
+
+ return seq_list_start(&iter->mscs->entries, *pos - 1);
+}
+
+static void *sta_mscs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (v == SEQ_START_TOKEN) {
+ ++*pos;
+ return seq_list_start(&iter->mscs->entries, 0);
+ }
+
+ return seq_list_next(v, &iter->mscs->entries, pos);
+}
+
+static void sta_mscs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (iter->mscs)
+ spin_unlock_bh(&iter->mscs->lock);
+
+ rcu_read_unlock();
+}
+
+static int sta_mscs_seq_show(struct seq_file *s, void *v)
+{
+ struct sta_mscs_iter *iter = s->private;
+ struct ieee80211_mscs_sta *mscs = iter->mscs;
+ struct ieee80211_flow_entry *entry;
+
+ if (v == SEQ_START_TOKEN) {
+ seq_printf(s,
+ "up_bitmap 0x%02x up_limit %u timeout %ums entries %u\n",
+ mscs->up_bitmap, mscs->up_limit,
+ jiffies_to_msecs(mscs->timeout), mscs->n_entries);
+ return 0;
+ }
+
+ entry = list_entry(v, struct ieee80211_flow_entry, list);
+
+ seq_printf(s, " up %u age %ums", READ_ONCE(entry->up),
+ jiffies_to_msecs(jiffies - READ_ONCE(entry->last_update)));
+ sta_flow_key_show(s, &entry->hkey.key, mscs->layout);
+ seq_putc(s, '\n');
+
+ return 0;
+}
+
+static const struct seq_operations sta_mscs_seq_ops = {
+ .start = sta_mscs_seq_start,
+ .next = sta_mscs_seq_next,
+ .stop = sta_mscs_seq_stop,
+ .show = sta_mscs_seq_show,
+};
+
+static int sta_mscs_open(struct inode *inode, struct file *file)
+{
+ struct sta_mscs_iter *iter;
+
+ iter = __seq_open_private(file, &sta_mscs_seq_ops, sizeof(*iter));
+ if (!iter)
+ return -ENOMEM;
+
+ iter->sta = inode->i_private;
+
+ return 0;
+}
+
+static const struct file_operations sta_mscs_ops = {
+ .open = sta_mscs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release_private,
+};
+
#define AQM_TXQ_ENTRY_LEN 130
static ssize_t sta_aqm_read(struct file *file, char __user *userbuf,
@@ -1252,6 +1488,8 @@ void ieee80211_sta_debugfs_add(struct sta_info *sta)
sta->debugfs_dir = debugfs_create_dir(mac, stations_dir);
DEBUGFS_ADD(flags);
+ DEBUGFS_ADD(scs);
+ DEBUGFS_ADD(mscs);
DEBUGFS_ADD(aid);
DEBUGFS_ADD(num_ps_buf_frames);
DEBUGFS_ADD(last_seq_ctrl);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 14/15] wifi: mac80211_hwsim: support SCS and MSCS
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (12 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 13/15] wifi: mac80211: add SCS and MSCS debugfs files Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
2026-09-02 8:46 ` [RFC PATCH 15/15] wifi: mt76: mt7996: apply the SCS traffic description Felix Fietkau
[not found] ` <4c3aee64-d289-4648-9520-edb6518b3d13@oss.qualcomm.com>
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
Let the stack be exercised without hardware. mac80211 does both services
in software, so the two extended feature flags are all a simulated device
needs.
It answers the traffic descriptions as well, because mac80211 declines
every descriptor carrying one when no driver does. Nothing here schedules
a stream, so the answer comes from debugfs: scs_status holds the status
code to give such a descriptor, and zero accepts it.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/virtual/mac80211_hwsim_i.h | 3 ++-
drivers/net/wireless/virtual/mac80211_hwsim_main.c | 25 +++++++++++++++-
2 files changed, 28 insertions(+)
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_i.h b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
index 0f0f2ac6d80e..f9316a24a639 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_i.h
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
@@ -137,6 +137,9 @@ struct mac80211_hwsim_data {
/* RSSI in rx status of the receiver */
int rx_rssi;
+ /* IEEE status code to answer a traffic description with, 0 accepts */
+ u16 scs_status;
+
/* only used when pmsr capability is supplied */
struct cfg80211_pmsr_capabilities pmsr_capa;
struct cfg80211_pmsr_request *pmsr_request;
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index 02b6d81cccd1..ee9a78853211 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -4289,6 +4289,27 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
return 0;
}
+/*
+ * Nothing here serves a traffic description, so there is nothing to run out of
+ * and no reason of its own to refuse one. The answer comes from debugfs, so a
+ * test can drive what mac80211 does with either one.
+ */
+static int mac80211_hwsim_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct mac80211_hwsim_data *data = hw->priv;
+ u8 i;
+
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = data->scs_status;
+
+ return 0;
+}
+
#ifdef CONFIG_MAC80211_DEBUGFS
#define HWSIM_DEBUGFS_OPS \
.link_add_debugfs = mac80211_hwsim_link_add_debugfs,
@@ -4313,6 +4334,7 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
.link_sta_rc_update = mac80211_hwsim_sta_rc_update, \
.conf_tx = mac80211_hwsim_conf_tx, \
.get_survey = mac80211_hwsim_get_survey, \
+ .sta_set_scs = mac80211_hwsim_sta_set_scs, \
CFG80211_TESTMODE_CMD(mac80211_hwsim_testmode_cmd) \
.ampdu_action = mac80211_hwsim_ampdu_action, \
.flush = mac80211_hwsim_flush, \
@@ -5967,6 +5989,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
NL80211_EXT_FEATURE_EXT_KEY_ID);
wiphy_ext_feature_set(hw->wiphy,
NL80211_EXT_FEATURE_ASSOC_FRAME_ENCRYPTION);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_SCS);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_MSCS);
hw->wiphy->interface_modes = param->iftypes;
@@ -6156,6 +6180,7 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
&hwsim_fops_group);
debugfs_create_file("rx_rssi", 0666, data->debugfs, data,
&hwsim_fops_rx_rssi);
+ debugfs_create_u16("scs_status", 0600, data->debugfs, &data->scs_status);
if (!data->use_chanctx)
debugfs_create_file("dfs_simulate_radar", 0222,
data->debugfs,
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread* [RFC PATCH 15/15] wifi: mt76: mt7996: apply the SCS traffic description
2026-09-02 8:45 [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
` (13 preceding siblings ...)
2026-09-02 8:46 ` [RFC PATCH 14/15] wifi: mac80211_hwsim: support SCS and MSCS Felix Fietkau
@ 2026-09-02 8:46 ` Felix Fietkau
[not found] ` <4c3aee64-d289-4648-9520-edb6518b3d13@oss.qualcomm.com>
15 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-02 8:46 UTC (permalink / raw)
To: linux-wireless; +Cc: johannes
The firmware schedules uplink traffic against the QoS Characteristics
element of an SCS stream, so pass the element down through sta_set_scs
and let the MURU scheduler serve the stream with trigger frames.
A downlink or direct link descriptor is declined, because the firmware
serves uplink alone.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/init.c | 1 +-
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 79 +++++++++++++++-
drivers/net/wireless/mediatek/mt76/mt7996/mcu.c | 40 ++++++++-
drivers/net/wireless/mediatek/mt76/mt7996/mcu.h | 4 +-
drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h | 2 +-
5 files changed, 126 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index fb635a092584..fb064fb4f938 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -526,6 +526,7 @@ mt7996_init_wiphy(struct ieee80211_hw *hw, struct mtk_wed_device *wed)
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_CAN_REPLACE_PTK0);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MU_MIMO_AIR_SNIFFER);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SCS);
if (mt7996_eeprom_has_background_radar(dev) &&
(!mdev->dev->of_node ||
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e218856b0c45..61c89b1aa4cb 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -2051,6 +2051,84 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
mutex_unlock(&dev->mt76.mutex);
}
+/*
+ * A stream belongs to the station and not to one of its links, so every link
+ * carries it. A link with no station entry is not one that can serve it, so a
+ * descriptor that reached none of them has not been programmed at all.
+ */
+static int mt7996_sta_scs_program(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ const struct cfg80211_scs_desc *desc,
+ bool del)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct ieee80211_link_sta *link_sta;
+ unsigned int link_id;
+ bool any = false;
+ int ret = 0;
+
+ for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link || !msta_link->wcid.sta)
+ continue;
+
+ any = true;
+ if (mt7996_mcu_set_scs(dev, msta_link->wcid.idx, desc, del))
+ ret = -EIO;
+ }
+
+ return any ? ret : -EIO;
+}
+
+static int mt7996_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct mt7996_dev *dev = mt7996_hw_dev(hw);
+ u8 i;
+
+ mutex_lock(&dev->mt76.mutex);
+
+ for (i = 0; i < n_desc; i++) {
+ const struct cfg80211_scs_desc *d = desc[i];
+ bool del = d->req_type == NL80211_SCS_REQ_REMOVE || !d->qos_char;
+
+ /*
+ * The firmware serves an uplink stream with trigger frames and
+ * has nothing for the other two directions. 11.25.2 leaves a
+ * declined change with its earlier classification, so the
+ * stream that went with it stays too.
+ */
+ if (!del && ieee80211_qos_char_direction(d->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_UPLINK) {
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ continue;
+ }
+
+ /*
+ * A descriptor with no traffic description leaves mac80211 to
+ * classify alone, so whatever the identifier was serving is
+ * deleted rather than left running. Nothing about a deletion
+ * can be reported, and nothing needs to be.
+ */
+ if (!mt7996_sta_scs_program(dev, vif, sta, d, del) || del)
+ continue;
+
+ /* No link may serve a stream the station is told it lost */
+ mt7996_sta_scs_program(dev, vif, sta, d, true);
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
+ mutex_unlock(&dev->mt76.mutex);
+
+ return 0;
+}
+
static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
@@ -2588,6 +2666,7 @@ const struct ieee80211_ops mt7996_ops = {
.set_coverage_class = mt7996_set_coverage_class,
.sta_statistics = mt7996_sta_statistics,
.sta_set_4addr = mt7996_sta_set_4addr,
+ .sta_set_scs = mt7996_sta_set_scs,
.sta_set_decap_offload = mt7996_sta_set_decap_offload,
.add_twt_setup = mt7996_mac_add_twt_setup,
.twt_teardown_request = mt7996_twt_teardown_request,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index cd7587ce5b8a..2af84a6ab9e4 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2616,6 +2616,46 @@ int mt7996_mcu_add_rate_ctrl(struct mt7996_dev *dev, struct mt7996_sta *msta,
return ret;
}
+/* The firmware reads the TID and the rest of the stream out of the element */
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del)
+{
+#define MURU_QOS_FLAG_UPDATE 20
+#define MURU_QOS_FLAG_DELETE 21
+ struct {
+ u8 __rsv1[4];
+
+ __le16 tag;
+ __le16 len;
+ __le32 qos_flag;
+ __le16 wlan_idx;
+ u8 __rsv2[12];
+ u8 dir;
+ u8 __rsv3[4];
+ u8 scs_id;
+ u8 qos_ie[44];
+ } __packed req = {
+ .tag = cpu_to_le16(UNI_CMD_MURU_SET_QOS_CFG),
+ .len = cpu_to_le16(sizeof(req) - 4),
+ .wlan_idx = cpu_to_le16(wlan_idx),
+ .scs_id = desc->id,
+ };
+
+ if (del) {
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_DELETE);
+ } else {
+ if (desc->qos_char_len > sizeof(req.qos_ie))
+ return -EINVAL;
+
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_UPDATE);
+ req.dir = ieee80211_qos_char_direction(desc->qos_char);
+ memcpy(req.qos_ie, desc->qos_char, desc->qos_char_len);
+ }
+
+ return mt76_mcu_send_msg(&dev->mt76, MCU_WMWA_UNI_CMD(MURU), &req,
+ sizeof(req), true);
+}
+
static int
mt7996_mcu_add_group(struct mt7996_dev *dev, struct mt7996_vif_link *link,
struct mt76_wcid *wcid)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 74b70fb6da3d..efd474cf8da9 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -991,6 +991,10 @@ enum {
};
enum {
+ UNI_CMD_MURU_SET_QOS_CFG = 0xfe,
+};
+
+enum {
UNI_VOW_DRR_CTRL,
UNI_VOW_RX_AT_AIRTIME_EN = 0x0b,
UNI_VOW_RX_AT_AIRTIME_CLR_EN = 0x0e,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 2a0cdb56f822..ebd55d8da654 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -754,6 +754,8 @@ int mt7996_mcu_set_fixed_rate_ctrl(struct mt7996_dev *dev,
void *data, u16 version);
int mt7996_mcu_set_fixed_field(struct mt7996_dev *dev, struct mt7996_sta *msta,
void *data, u8 link_id, u32 field);
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del);
int mt7996_mcu_set_eeprom(struct mt7996_dev *dev);
int mt7996_mcu_get_eeprom(struct mt7996_dev *dev, u32 offset, u8 *buf, u32 buf_len,
enum mt7996_eeprom_mode mode);
--
git-series 0.9.1
^ permalink raw reply related [flat|nested] 19+ messages in thread[parent not found: <4c3aee64-d289-4648-9520-edb6518b3d13@oss.qualcomm.com>]
* Re: [RFC PATCH 00/15] wifi: SCS and MSCS
[not found] ` <4c3aee64-d289-4648-9520-edb6518b3d13@oss.qualcomm.com>
@ 2026-09-09 13:55 ` Felix Fietkau
2026-09-13 19:23 ` Vivek Chettri
0 siblings, 1 reply; 19+ messages in thread
From: Felix Fietkau @ 2026-09-09 13:55 UTC (permalink / raw)
To: Bhagavathi Perumal S, linux-wireless; +Cc: johannes, ripan.deuri, Vivek Chettri
Hi,
thanks for your feedback. Comments below.
On 09.09.26 15:05, Bhagavathi Perumal S wrote:
> SCS and MSCS are primarily mechanisms for negotiating QoS treatment
> between an AP and a STA. A negotiated policy consists of:
>
> 1.a traffic classifier, such as the applicable TCLAS or flow parameters; and
>
> 2.the QoS treatment to apply to matching traffic, such as the User
> Priority/TID and, where applicable, other QoS characteristics.
>
> The classifier and the QoS treatment should be considered separately
> from the protocol state.
I'm not sure I agree. From my understanding of the standard, acceptance
and lifetime of the classifier data and the QoS characteristics must be
considered together. If applying characteristics fails, the full
descriptor (including its classifier information) must be rejected.
Handling this together in one place seems beneficial.
> The protocol state needs to be maintained so that the AP can validate
> requests, generate the appropriate responses,
>
> handle updates and deletions, and associate the policy with the correct
> STA and link.
>
> For IP traffic, classification is based primarily on L3/L4 information—
> for example, IPv4/IPv6 addresses, protocol, ports, DSCP, and related
> flow attributes.
>
> Linux already provides mature packet-classification mechanisms through
> netfilter/nftables, conntrack, flow tables, and vendor-specific flow-
> offload infrastructure.
>
> These mechanisms can classify the traffic and attach the required QoS
> information(TID or additional QoS attributes) for mac80211 and the
> vendor driver to consume.
>
> Therefore, the packet-classification implementation does not need to be
> duplicated in mac80211.
>
> mac80211 should not independently reimplement a second L3/L4 classifier
> when the same classification can already be performed by the networking
> stack or by an accelerated data path.
From my perspective, it's not as simple as that. Re-implementation is
perfectly acceptable if the part that's being re-implemented is not a
good fit for the use case and if the end result is more functional and
less complex.
I believe this to be the case here. Consider the following data points:
In my implementation, net/wireless/scs.c (which implements the full
classifier) is 586 lines of code.
net/mac80211/scs.c, which handles glue for maintaining the data
structures and the outer classifier call is less than 400 lines.
In the hostapd implementation from QSDK 14, the nft.c glue code alone
(which is just one part of the classifier related code) is ~1350 lines.
robust_av.c (which is ~2060 lines) adds even more classifier related code.
With that in mind, reusing nft already seems significantly more complex
than a narrowly scoped classifier implementation written specifically
for SCS/MSCS.
> mac80211 may still need to own the wireless-specific policy state and
> provide an interface for:
>
> 1. installing, updating, and removing a negotiated SCS/MSCS policy;
> 2. associating the policy with a particular STA;
> 3. reporting whether the policy was accepted, rejected, or could not be
> enforced; and
> 4. handling station disassociation, policy update, and teardown.
>
> The actual packet matching can then remain in the existing packet-
> classification infrastructure.
>
> This avoids duplicating classifier logic in mac80211 and allows the same
> policy to be used consistently for both software and accelerated paths.
So far the only working upstream acceleration path from Ethernet to WLAN
is for MTK hardware. Having written (though not yet published) an
implementation to handle SCS/MSCS for offloaded flows, I can tell you
that it's much more straightforward to do this in the kernel based on my
series. It allows handling both netfilter based flows (L3 routed) and L2
bridge offloaded flows via tc-flower consistently in the same way.
Especially the L2 tc-flower offload case would be hard to handle with
your nft based proposal in a way that handles runtime flow updates well.
> If CPU overhead is a concern, it should be evaluated using profiling
> data. In particular, the evaluation should compare:
>
> 1. classification with and without SCS/MSCS rules;
> 2. the first-packet/flow-learning cost;
> 3. the per-packet cost after flow establishment;
> 4. the number of active rules and stations; and
> 5. the effect of conntrack, nftables flow tables, and hardware offload.
> 6. For hardware-accelerated paths, the packet-classification framework
> can notify or program the accelerator when a matching flow is learned.
Again, not as simple as that. There's inherent cost to using an
implementation that uses dynamic dispatch and is written for a lot of
different use cases, vs a static fixed-purpose classifier.
> This architecture keeps the protocol and wireless-specific policy
> handling in the Wi-Fi stack
>
> while reusing the existing Linux networking and flow-offload
> infrastructure for L3/L4 classification.
>
> It avoids duplicate classification logic and provides a clear path for
> both software and hardware-accelerated data paths.
I don't see the clear path for the hardware-accelerated data path at
all. Could you elaborate?
- Felix
^ permalink raw reply [flat|nested] 19+ messages in thread* Re: [RFC PATCH 00/15] wifi: SCS and MSCS
2026-09-09 13:55 ` [RFC PATCH 00/15] wifi: SCS and MSCS Felix Fietkau
@ 2026-09-13 19:23 ` Vivek Chettri
2026-09-13 21:19 ` Felix Fietkau
0 siblings, 1 reply; 19+ messages in thread
From: Vivek Chettri @ 2026-09-13 19:23 UTC (permalink / raw)
To: nbd@nbd.name, Bhagavathi Perumal S,
linux-wireless@vger.kernel.org
Cc: johannes@sipsolutions.net, ripan.deuri@oss.qualcomm.com,
vivek.chettri@oss.qualcomm.com
Hi,
The preference for reusing existing packet-classification infrastructure is driven by the need to have a centralized policy management and policy framework that operates across both software and hardware-offloaded forwarding paths, rather than introducing a separate classifier within the WLAN stack.
1. Centralized QoS and Policy Management
A centralized policy management framework provides the following advantages:
• A centralized policy database enables end-to-end QoS policy enforcement across
different forwarding paths, improving manageability and policy visibility
throughout the system.
• SCS/MSCS policies represent only one source of policy management.
Administrators may also configure firewall, QoS, or other policies that must
coexist with negotiated WLAN policies.
• Centralized policy management provides clear visibility into the interaction
between administrator-defined policies and dynamically negotiated SCS/MSCS
policies, making conflicts easier to detect and manage.
• Implementing packet prioritization independently within mac80211 can introduce
policy conflicts or ambiguous precedence when administrator-defined QoS
policies overlap with SCS/MSCS-derived priorities.
• A common policy framework helps ensure consistent behavior across software
forwarding, bridging, routing, and hardware-offloaded data paths.
2. Policy Framework: Considerations and Challenges
A policy framework should help ensure consistent behavior across software
forwarding, bridging, routing, and hardware-offloaded data paths.
The policy framework can be realized using either nftables or tc-flower.
Both approaches provide a persistent and centralized repository for traffic
classification and policy enforcement, but differ in integration complexity,
feature scope, and deployment model.
Common Capabilities: nftables and tc-flower
• Both nftables and tc-flower maintain centralized rule databases for traffic
classification and policy enforcement.
• For software-forwarded traffic, both solutions require flow lookups against
installed rules and therefore have broadly comparable classification
overhead.
• Both mechanisms can serve as the policy enforcement layer for
SCS/MSCS-derived traffic policies.
Advantages of nftables
• nftables provides a unified framework for firewalling, traffic
classification, and QoS policy marking.
• nftables can support both software and hardware-accelerated forwarding
paths and can be extended to additional offload scenarios with minimal
architectural changes.
• Existing support for nftables-based policy handling in hostapd reduces
integration effort and leverages infrastructure that is already available
and upstream-ready.
The implementation is done using libmnl, which avoids licensing
incompatibility between hostapd (BSD-3-Clause) and libnftnl (GPLv2).
This approach was also reviewed with Jouni.
• Using nftables allows SCS/MSCS policies to be integrated into a broader,
system-wide policy framework rather than introducing a WLAN-specific
policy engine.
• Using nftables can also facilitate scalability by allowing these rules to
be applied at software acceleration decision-making stages. For example,
the NFT rule can be cleanly integrated into NFT flow tables as well.
Challenges with a tc-flower-based Approach
• A tc-flower solution would require substantial additional implementation
for rule programming from hostapd.
• tc-flower typically requires qdisc configuration (at a minimum, a clsact
pseudo qdisc) before classification rules can be installed and enforced.
3. Summary
• The primary role of SCS/MSCS handling within the WLAN subsystem should be
to negotiate and manage QoS policies between the AP and STA.
• Packet classification and policy enforcement should be delegated to a
centralized policy framework such as nftables, which is already designed
to manage system-wide traffic policies.
• The WLAN driver remains responsible for mapping classified traffic to the
appropriate transmission priorities and ensuring the negotiated QoS
behavior is achieved across the data path.
Also, what is the plan for supporting repeaters and backend devices?
Thanks!
Vivek.
________________________________________
From: Felix Fietkau <nbd@nbd.name>
Sent: Wednesday, September 9, 2026 7:25 PM
To: Bhagavathi Perumal S <bhagavathi.pillai@oss.qualcomm.com>; linux-wireless@vger.kernel.org <linux-wireless@vger.kernel.org>
Cc: johannes@sipsolutions.net <johannes@sipsolutions.net>; ripan.deuri@oss.qualcomm.com <ripan.deuri@oss.qualcomm.com>; Vivek Chettri <vchettri@qti.qualcomm.com>
Subject: Re: [RFC PATCH 00/15] wifi: SCS and MSCS
WARNING: This email originated from outside of Qualcomm. Please be wary of any links or attachments, and do not enable macros.
Hi,
thanks for your feedback. Comments below.
On 09.09.26 15:05, Bhagavathi Perumal S wrote:
> SCS and MSCS are primarily mechanisms for negotiating QoS treatment
> between an AP and a STA. A negotiated policy consists of:
>
> 1.a traffic classifier, such as the applicable TCLAS or flow parameters; and
>
> 2.the QoS treatment to apply to matching traffic, such as the User
> Priority/TID and, where applicable, other QoS characteristics.
>
> The classifier and the QoS treatment should be considered separately
> from the protocol state.
I'm not sure I agree. From my understanding of the standard, acceptance
and lifetime of the classifier data and the QoS characteristics must be
considered together. If applying characteristics fails, the full
descriptor (including its classifier information) must be rejected.
Handling this together in one place seems beneficial.
> The protocol state needs to be maintained so that the AP can validate
> requests, generate the appropriate responses,
>
> handle updates and deletions, and associate the policy with the correct
> STA and link.
>
> For IP traffic, classification is based primarily on L3/L4 information—
> for example, IPv4/IPv6 addresses, protocol, ports, DSCP, and related
> flow attributes.
>
> Linux already provides mature packet-classification mechanisms through
> netfilter/nftables, conntrack, flow tables, and vendor-specific flow-
> offload infrastructure.
>
> These mechanisms can classify the traffic and attach the required QoS
> information(TID or additional QoS attributes) for mac80211 and the
> vendor driver to consume.
>
> Therefore, the packet-classification implementation does not need to be
> duplicated in mac80211.
>
> mac80211 should not independently reimplement a second L3/L4 classifier
> when the same classification can already be performed by the networking
> stack or by an accelerated data path.
From my perspective, it's not as simple as that. Re-implementation is
perfectly acceptable if the part that's being re-implemented is not a
good fit for the use case and if the end result is more functional and
less complex.
I believe this to be the case here. Consider the following data points:
In my implementation, net/wireless/scs.c (which implements the full
classifier) is 586 lines of code.
net/mac80211/scs.c, which handles glue for maintaining the data
structures and the outer classifier call is less than 400 lines.
In the hostapd implementation from QSDK 14, the nft.c glue code alone
(which is just one part of the classifier related code) is ~1350 lines.
robust_av.c (which is ~2060 lines) adds even more classifier related code.
With that in mind, reusing nft already seems significantly more complex
than a narrowly scoped classifier implementation written specifically
for SCS/MSCS.
> mac80211 may still need to own the wireless-specific policy state and
> provide an interface for:
>
> 1. installing, updating, and removing a negotiated SCS/MSCS policy;
> 2. associating the policy with a particular STA;
> 3. reporting whether the policy was accepted, rejected, or could not be
> enforced; and
> 4. handling station disassociation, policy update, and teardown.
>
> The actual packet matching can then remain in the existing packet-
> classification infrastructure.
>
> This avoids duplicating classifier logic in mac80211 and allows the same
> policy to be used consistently for both software and accelerated paths.
So far the only working upstream acceleration path from Ethernet to WLAN
is for MTK hardware. Having written (though not yet published) an
implementation to handle SCS/MSCS for offloaded flows, I can tell you
that it's much more straightforward to do this in the kernel based on my
series. It allows handling both netfilter based flows (L3 routed) and L2
bridge offloaded flows via tc-flower consistently in the same way.
Especially the L2 tc-flower offload case would be hard to handle with
your nft based proposal in a way that handles runtime flow updates well.
> If CPU overhead is a concern, it should be evaluated using profiling
> data. In particular, the evaluation should compare:
>
> 1. classification with and without SCS/MSCS rules;
> 2. the first-packet/flow-learning cost;
> 3. the per-packet cost after flow establishment;
> 4. the number of active rules and stations; and
> 5. the effect of conntrack, nftables flow tables, and hardware offload.
> 6. For hardware-accelerated paths, the packet-classification framework
> can notify or program the accelerator when a matching flow is learned.
Again, not as simple as that. There's inherent cost to using an
implementation that uses dynamic dispatch and is written for a lot of
different use cases, vs a static fixed-purpose classifier.
> This architecture keeps the protocol and wireless-specific policy
> handling in the Wi-Fi stack
>
> while reusing the existing Linux networking and flow-offload
> infrastructure for L3/L4 classification.
>
> It avoids duplicate classification logic and provides a clear path for
> both software and hardware-accelerated data paths.
I don't see the clear path for the hardware-accelerated data path at
all. Could you elaborate?
- Felix
^ permalink raw reply [flat|nested] 19+ messages in thread* Re: [RFC PATCH 00/15] wifi: SCS and MSCS
2026-09-13 19:23 ` Vivek Chettri
@ 2026-09-13 21:19 ` Felix Fietkau
0 siblings, 0 replies; 19+ messages in thread
From: Felix Fietkau @ 2026-09-13 21:19 UTC (permalink / raw)
To: Vivek Chettri, Bhagavathi Perumal S,
linux-wireless@vger.kernel.org
Cc: johannes@sipsolutions.net, ripan.deuri@oss.qualcomm.com,
vivek.chettri@oss.qualcomm.com
Hi Vivek,
On 13.09.26 21:23, Vivek Chettri wrote:
> The preference for reusing existing packet-classification infrastructure is driven by the need to have a centralized policy management and policy framework that operates across both software and hardware-offloaded forwarding paths, rather than introducing a separate classifier within the WLAN stack.
>
> 1. Centralized QoS and Policy Management
>
> A centralized policy management framework provides the following advantages:
>
> • A centralized policy database enables end-to-end QoS policy enforcement across
> different forwarding paths, improving manageability and policy visibility
> throughout the system.
>
> • SCS/MSCS policies represent only one source of policy management.
> Administrators may also configure firewall, QoS, or other policies that must
> coexist with negotiated WLAN policies.
>
> • Centralized policy management provides clear visibility into the interaction
> between administrator-defined policies and dynamically negotiated SCS/MSCS
> policies, making conflicts easier to detect and manage.
>
> • Implementing packet prioritization independently within mac80211 can introduce
> policy conflicts or ambiguous precedence when administrator-defined QoS
> policies overlap with SCS/MSCS-derived priorities.
>
> • A common policy framework helps ensure consistent behavior across software
> forwarding, bridging, routing, and hardware-offloaded data paths.
I don't think any of this really conflicts with my proposal.
A full end-to-end policy framework seems out of scope for direct
implementation in hostapd anyway, so it should be an external component.
hostapd should allow deferring management and policy decisions of
client-requested SCS/MSCS exchanges to external components anyway to
support things like EasyMesh well.
Having this separation would help if you intend to push flow
classification for things like wired upstream links, which hostapd
should not worry about at all.
In such a scenario, a custom implementation could choose to omit passing
full classifiers to mac80211.
That said, for most common use cases, including standalone APs and
simpler EasyMesh implementations, I believe that using the mac80211
support that I added is the right choice due to the lower complexity and
overhead.
> 2. Policy Framework: Considerations and Challenges
>
> A policy framework should help ensure consistent behavior across software
> forwarding, bridging, routing, and hardware-offloaded data paths.
>
> The policy framework can be realized using either nftables or tc-flower.
> Both approaches provide a persistent and centralized repository for traffic
> classification and policy enforcement, but differ in integration complexity,
> feature scope, and deployment model.
>
> Common Capabilities: nftables and tc-flower
>
> • Both nftables and tc-flower maintain centralized rule databases for traffic
> classification and policy enforcement.
>
> • For software-forwarded traffic, both solutions require flow lookups against
> installed rules and therefore have broadly comparable classification
> overhead.
>
> • Both mechanisms can serve as the policy enforcement layer for
> SCS/MSCS-derived traffic policies.
>
> Advantages of nftables
>
> • nftables provides a unified framework for firewalling, traffic
> classification, and QoS policy marking.
>
> • nftables can support both software and hardware-accelerated forwarding
> paths and can be extended to additional offload scenarios with minimal
> architectural changes.
>
> • Existing support for nftables-based policy handling in hostapd reduces
> integration effort and leverages infrastructure that is already available
> and upstream-ready.
>
> The implementation is done using libmnl, which avoids licensing
> incompatibility between hostapd (BSD-3-Clause) and libnftnl (GPLv2).
> This approach was also reviewed with Jouni.
>
> • Using nftables allows SCS/MSCS policies to be integrated into a broader,
> system-wide policy framework rather than introducing a WLAN-specific
> policy engine.
>
> • Using nftables can also facilitate scalability by allowing these rules to
> be applied at software acceleration decision-making stages. For example,
> the NFT rule can be cleanly integrated into NFT flow tables as well.
>
> Challenges with a tc-flower-based Approach
>
> • A tc-flower solution would require substantial additional implementation
> for rule programming from hostapd.
>
> • tc-flower typically requires qdisc configuration (at a minimum, a clsact
> pseudo qdisc) before classification rules can be installed and enforced.
Most of these points are very abstract and in my opinion gloss over a
lot of implementation complexity. I'd argue that just because nftables
is used to enable offloading in some setups doesn't mean that it handles
the different policy aspects of SCS and especially MSCS well, or
provides good ways to deal with runtime changes to existing flows.
I get the impression that your proposal is designed to handle the needs
of a single Qualcomm implementation of an out-of-tree policy framework
service well, while neglecting in-tree kernel offload mechanisms, as
well as standalone AP implementations by pushing extra complexity there.
Let me give you a specific example. On OpenWrt, Ethernet -> WLAN bridge
hw flow acceleration works by having a user space daemon that replicates
the bridge state, measures L2 flows via eBPF and programs no-sw
tc-flower offload rules for L2 flows.
On the MTK driver side, the offload core uses the programmed L2 rules to
trigger actual hw forwarding.
Only for routing/NAT flow offload does nftables get used.
The kernel side is fully implemented in upstream kernels without any
out-of-tree subsystems.
The forwarding from Ethernet to WLAN works because the kernel code can
resolve the hardware path via dev_fill_forward_path and query the
relevant metadata (which happens by calling into the WLAN driver through
mac80211).
In the series that I've built on top of this one (but not yet posted),
the offload core driver can additionally query the classifier result for
a flow at the point where it enables actual offloading (the trigger is
PPS threshold exceeded). This works because mac80211 has the active
classifiers, allowing the WLAN driver to provide them to the offload
core driver.
This design allows transparently respecting the SCS/MSCS classifier for
hw offloaded flows regardless of which API was used to trigger offload
(tc or nftables).
Additionally, the user space components handling the offload for both
approches do not have to worry about SCS/MSCS in any way.
I don't see how your hostapd programmed nftables rules approach would
handle the same properly.
> 3. Summary
>
> • The primary role of SCS/MSCS handling within the WLAN subsystem should be
> to negotiate and manage QoS policies between the AP and STA.
>
> • Packet classification and policy enforcement should be delegated to a
> centralized policy framework such as nftables, which is already designed
> to manage system-wide traffic policies.
>
> • The WLAN driver remains responsible for mapping classified traffic to the
> appropriate transmission priorities and ensuring the negotiated QoS
> behavior is achieved across the data path.
>
> Also, what is the plan for supporting repeaters and backend devices?
Not sure what you mean by backend devices. For repeaters I would use a
software component (preferably outside of hostapd/wpa_supplicant but
interacting with both), which acts as a repeater for SCS policies
requested by local clients (pushing them to the upstream AP) and tells
hostapd to install local descriptors where local classification is needed.
If you have concerns about specific scenarios, please describe them to me.
Thanks,
- Felix
^ permalink raw reply [flat|nested] 19+ messages in thread