diff --git a/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json b/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json
index f907211348..70098394c2 100644
--- a/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json
+++ b/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json
@@ -1,8 +1,5 @@
{
"eth0": {
"phy-detached-when-down": true
- },
- "eth1": {
- "broken-mqprio": true
}
}
diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig
index 17be58e53a..8812a1ba53 100644
--- a/board/aarch64/linux_defconfig
+++ b/board/aarch64/linux_defconfig
@@ -245,13 +245,22 @@ CONFIG_VLAN_8021Q_GVRP=y
CONFIG_VLAN_8021Q_MVRP=y
CONFIG_NET_SCHED=y
CONFIG_NET_SCH_MQPRIO=y
+CONFIG_NET_SCH_ETS=y
+CONFIG_NET_SCH_TBF=y
+CONFIG_NET_SCH_INGRESS=y
CONFIG_NET_CLS_BASIC=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_CLS_FLOWER=y
+CONFIG_NET_CLS_MATCHALL=y
+CONFIG_NET_EMATCH=y
+CONFIG_NET_EMATCH_CMP=y
+CONFIG_NET_EMATCH_META=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_ACT_GACT=y
CONFIG_NET_ACT_MIRRED=y
CONFIG_NET_ACT_SKBEDIT=y
+CONFIG_NET_ACT_PEDIT=y
+CONFIG_NET_ACT_CSUM=y
CONFIG_DCB=y
CONFIG_NETLINK_DIAG=y
CONFIG_MPLS=y
diff --git a/board/aarch64/microchip-ev23x71a/README.md b/board/aarch64/microchip-ev23x71a/README.md
index 166a6dfa6c..6de8c40f1c 100644
--- a/board/aarch64/microchip-ev23x71a/README.md
+++ b/board/aarch64/microchip-ev23x71a/README.md
@@ -26,7 +26,7 @@ Supported:
Not yet done:
- TSN queueing. PSFP and time aware shaping need `NET_SCH_TAPRIO`,
- `NET_ACT_GATE`, `NET_SCH_ETS`, and `NET_SCH_CBS`, all currently off
+ `NET_ACT_GATE`, and `NET_SCH_CBS`, all currently off
- HSR/PRP offload, see above
- MAC addresses from OTP. With no environment in flash the board falls
back to `lan969x_otp_get_mac()`, which derives 30 addresses for this
diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig
index 2bf55bd4ac..b504fa4851 100644
--- a/board/arm/linux_defconfig
+++ b/board/arm/linux_defconfig
@@ -229,13 +229,22 @@ CONFIG_VLAN_8021Q_GVRP=y
CONFIG_VLAN_8021Q_MVRP=y
CONFIG_NET_SCHED=y
CONFIG_NET_SCH_MQPRIO=y
+CONFIG_NET_SCH_ETS=y
+CONFIG_NET_SCH_TBF=y
+CONFIG_NET_SCH_INGRESS=y
CONFIG_NET_CLS_BASIC=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_CLS_FLOWER=y
+CONFIG_NET_CLS_MATCHALL=y
+CONFIG_NET_EMATCH=y
+CONFIG_NET_EMATCH_CMP=y
+CONFIG_NET_EMATCH_META=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_ACT_GACT=y
CONFIG_NET_ACT_MIRRED=y
CONFIG_NET_ACT_SKBEDIT=y
+CONFIG_NET_ACT_PEDIT=y
+CONFIG_NET_ACT_CSUM=y
CONFIG_DCB=y
CONFIG_NETLINK_DIAG=y
CONFIG_MPLS=y
diff --git a/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio b/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio
deleted file mode 100755
index 875dcc0b95..0000000000
--- a/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/sh
-# Opportunistically look for interfaces with multiple transmit queues
-# and hardware support for the mqprio queuing discipline. For every
-# matching interface, set up mappings from kernel-internal packet
-# priorities, via traffic classes, to transmit queues such that as
-# many high priorities as possible are scheduled on separate queues.
-
-set -e
-
-map()
-{
- case "$1" in
- 2)
- echo "map 0 0 0 0 0 0 1 1";;
- 3)
- echo "map 0 0 0 0 1 1 2 2";;
- 4)
- echo "map 0 0 1 1 2 2 3 3";;
- 5)
- echo "map 0 0 1 1 2 2 3 4";;
- 6)
- echo "map 0 0 1 1 2 3 4 5";;
- 7)
- echo "map 0 0 1 2 3 4 5 6";;
- 8)
- echo "map 0 1 2 3 4 5 6 7";;
- esac
-}
-
-queues()
-{
- out="queues "
- for tc in $(seq 0 $(($1 - 1))); do
- out="$out 1@$tc"
- done
-
- echo "$out"
-}
-
-quirks=
-[ -f /etc/product/interface-quirks.json ] && quirks=yes
-
-set $(ip -j -d link show | jq -r '.[] | .ifname, .num_tx_queues')
-while [ "$1" ]; do
- iface="$1"
- txqs="$2"
- shift 2
-
- [ "$quirks" ] && [ $(/usr/libexec/infix/has-quirk "broken-mqprio" "$iface") = "true" ] && echo "Skipping $iface, does not support mqprio" && continue
- [ $txqs -lt 2 ] && continue
- [ $txqs -gt 8 ] && txqs=8
-
- output=$(tc qdisc add dev $iface root mqprio hw 1 \
- num_tc $txqs $(map $txqs) $(queues $txqs) 2>&1) || true
- if echo "$output" | grep -q "does not support hardware offload"; then
- echo "Skipping $iface, hardware offload not supported."
- elif [ -n "$output" ]; then
- echo "$output"
- fi
-done
diff --git a/board/riscv64/linux_defconfig b/board/riscv64/linux_defconfig
index 1e88a6590f..de01d3f851 100644
--- a/board/riscv64/linux_defconfig
+++ b/board/riscv64/linux_defconfig
@@ -233,13 +233,22 @@ CONFIG_VLAN_8021Q_GVRP=y
CONFIG_VLAN_8021Q_MVRP=y
CONFIG_NET_SCHED=y
CONFIG_NET_SCH_MQPRIO=y
+CONFIG_NET_SCH_ETS=y
+CONFIG_NET_SCH_TBF=y
+CONFIG_NET_SCH_INGRESS=y
CONFIG_NET_CLS_BASIC=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_CLS_FLOWER=y
+CONFIG_NET_CLS_MATCHALL=y
+CONFIG_NET_EMATCH=y
+CONFIG_NET_EMATCH_CMP=y
+CONFIG_NET_EMATCH_META=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_ACT_GACT=y
CONFIG_NET_ACT_MIRRED=y
CONFIG_NET_ACT_SKBEDIT=y
+CONFIG_NET_ACT_PEDIT=y
+CONFIG_NET_ACT_CSUM=y
CONFIG_DCB=y
CONFIG_NETLINK_DIAG=y
CONFIG_MPLS=y
diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig
index 0d5a47032e..ea47ca7ce6 100644
--- a/board/x86_64/linux_defconfig
+++ b/board/x86_64/linux_defconfig
@@ -223,9 +223,23 @@ CONFIG_VLAN_8021Q=y
CONFIG_VLAN_8021Q_GVRP=y
CONFIG_VLAN_8021Q_MVRP=y
CONFIG_NET_SCHED=y
+CONFIG_NET_SCH_MQPRIO=y
+CONFIG_NET_SCH_ETS=y
+CONFIG_NET_SCH_TBF=y
+CONFIG_NET_SCH_INGRESS=y
CONFIG_NET_CLS_BASIC=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_CLS_FLOWER=y
+CONFIG_NET_CLS_MATCHALL=y
+CONFIG_NET_EMATCH=y
+CONFIG_NET_EMATCH_CMP=y
+CONFIG_NET_EMATCH_META=y
+CONFIG_NET_CLS_ACT=y
+CONFIG_NET_ACT_GACT=y
+CONFIG_NET_ACT_SKBEDIT=y
+CONFIG_NET_ACT_PEDIT=y
+CONFIG_NET_ACT_CSUM=y
+CONFIG_DCB=y
CONFIG_NETLINK_DIAG=y
CONFIG_MPLS=y
CONFIG_NET_MPLS_GSO=y
diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index 95946eb718..cc5a09de3e 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -192,6 +192,22 @@ All notable changes to the project are documented in this file.
- LAN969x boards ship `symreg`, which reads and writes switch registers by
name and dumps the MAC, VLAN and VCAP tables, for debugging the switch
core, see `board/aarch64/microchip-lan969x/README.md`
+- Add per-interface Quality of Service configuration: which header field
+ a port trusts (PCP, DSCP, or both in order), the default priority, PCP
+ and DSCP to priority maps with standard presets, an egress traffic
+ class table with strict-priority or weighted transmission selection per
+ class, and PCP and DSCP remarking on transmit. The defaults are on out
+ of the box on every port: trust PCP, one traffic class per
+ transmit queue mapped per IEEE 802.1Q-2022 Table 8-5, replacing the
+ fixed boot-time queue setup, see [QoS][]
+- Add a per-port egress rate limit to the new [QoS][] subsystem
+- Transmission selection uses the `ets` scheduler on every
+ port, so drivers that offload it take the class algorithms and weights
+ into the switch fabric, not only the priority to class map
+- Marvell LinkStreet 88E6390 and 88E6393X switch ports offload QoS
+ classification, trust order, remarking, transmission selection and the
+ egress rate limit to the switch, so they apply to hardware forwarded
+ traffic as well
### Fixes
@@ -353,6 +369,7 @@ All notable changes to the project are documented in this file.
[fdb]: https://www.kernelkit.org/infix/latest/bridging/#unicast-forwarding-database
[boot]: https://www.kernelkit.org/infix/latest/boot/
[mcast]: https://www.kernelkit.org/infix/latest/bridging/#multicast-filtering-and-snooping
+[QoS]: https://www.kernelkit.org/infix/latest/qos/
[v26.09.0][] - 2026-09-30
-------------------------
diff --git a/doc/extra.css b/doc/extra.css
index 3e9c836df6..f22340f1c8 100644
--- a/doc/extra.css
+++ b/doc/extra.css
@@ -102,14 +102,26 @@ body {
--md-primary-bg-color: #5c5f5c;
}
-/* Center Markdown Tables (requires md_in_html extension) */
-.center-table {
+/* Caption numbers in bold: "Figure 1:", "Table 1:" */
+.md-typeset figcaption .caption-prefix {
+ font-weight: 700;
+}
+
+/* Center all Markdown tables, like figures */
+.md-typeset .md-typeset__scrollwrap {
text-align: center;
}
-.md-typeset .center-table :is(td,th):not([align]) {
- /* Reset alignment for table cells */
- text-align: initial;
+/* Material forces a 5rem minimum width on header cells, which makes
+ * narrow numeric tables sprawl past the page. Fit headers to content. */
+.md-typeset table:not([class]) th {
+ min-width: 0;
+}
+
+/* Material's 1.4 line height leaves gaps between box-drawing characters
+ * in CLI output and diagrams. Applies to code blocks as a whole. */
+.md-typeset pre {
+ line-height: 1.15;
}
/* CLI terminal output styling */
@@ -122,3 +134,60 @@ body {
color: #1a1a1a;
font-weight: bold;
}
+
+/* ---- Print ----------------------------------------------------------------
+ * Material hides the chrome when printing; the rest is page layout. */
+@page {
+ size: A4;
+ margin: 18mm 16mm;
+}
+
+@media print {
+ /* Keep tables, figures, code and admonitions in one piece. Caption
+ * blocks wrap the table in a figure, so the caption stays with it. */
+ .md-typeset figure,
+ .md-typeset .md-typeset__table,
+ .md-typeset pre,
+ .md-typeset .admonition {
+ break-inside: avoid;
+ }
+
+ /* Headings travel with what follows them, see print-keep.js */
+ .md-typeset h1,
+ .md-typeset h2,
+ .md-typeset h3,
+ .md-typeset h4 {
+ break-after: avoid;
+ }
+
+ .print-keep {
+ break-inside: avoid;
+ }
+
+ .md-typeset pre.cli .header {
+ print-color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+ }
+
+ /* Material's palette only switches colours on screen, so a reader on
+ * the dark palette would print light grey text on white paper. */
+ [data-md-color-scheme="slate"] {
+ --md-default-bg-color: #fff;
+ --md-default-fg-color: #34343c;
+ --md-default-fg-color--light: #54545c;
+ --md-default-fg-color--lighter: rgb(0 0 0 / 32%);
+ --md-default-fg-color--lightest: rgb(0 0 0 / 7%);
+ --md-typeset-color: #34343c;
+ --md-typeset-a-color: #0056b2;
+ --md-code-bg-color: #f6f8fa;
+ --md-code-fg-color: #3a3a3a;
+ color-scheme: light;
+ }
+
+ [data-md-color-scheme="slate"] .md-typeset h1,
+ [data-md-color-scheme="slate"] .md-typeset h2,
+ [data-md-color-scheme="slate"] .md-typeset h3,
+ [data-md-color-scheme="slate"] .md-typeset h4 {
+ color: #2a2a2a;
+ }
+}
diff --git a/doc/img/qos-pipeline.svg b/doc/img/qos-pipeline.svg
new file mode 100644
index 0000000000..88c256ac0b
--- /dev/null
+++ b/doc/img/qos-pipeline.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/doc/print-keep.js b/doc/print-keep.js
new file mode 100644
index 0000000000..7d647957a9
--- /dev/null
+++ b/doc/print-keep.js
@@ -0,0 +1,52 @@
+/* Keep a heading on the same page as what follows it when printing.
+ *
+ * Browsers cannot keep siblings together, and Firefox ignores
+ * break-after: avoid, so before printing each heading is wrapped with
+ * its first block, plus a short introduction and the figure, table or
+ * code block it introduces, in a div that must not break. The wrap is
+ * undone after printing. */
+(function () {
+ const BLOCK = "figure, table, .md-typeset__scrollwrap, .highlight, pre, .admonition, details";
+ let groups = [];
+
+ /* A short paragraph, or one ending in a colon, introduces what follows */
+ function intro(el) {
+ if (!el || el.tagName !== "P")
+ return false;
+ const text = el.textContent.trim();
+ return text.endsWith(":") || text.length < 240;
+ }
+
+ function keep() {
+ unkeep();
+ const heads = document.querySelectorAll(".md-typeset :is(h1, h2, h3, h4, h5, h6)");
+ for (const h of Array.from(heads).reverse()) {
+ const g = document.createElement("div");
+ let next = h.nextElementSibling;
+
+ g.className = "print-keep";
+ h.replaceWith(g);
+ g.appendChild(h);
+ groups.push(g);
+ if (!next)
+ continue;
+
+ g.appendChild(next);
+ while (intro(next)) {
+ next = g.nextElementSibling;
+ if (!next || !(intro(next) || next.matches(BLOCK)))
+ break;
+ g.appendChild(next);
+ }
+ }
+ }
+
+ function unkeep() {
+ for (const g of groups)
+ g.replaceWith(...g.childNodes);
+ groups = [];
+ }
+
+ addEventListener("beforeprint", keep);
+ addEventListener("afterprint", unkeep);
+})();
diff --git a/doc/qos.md b/doc/qos.md
index d937104f67..4027bf413e 100644
--- a/doc/qos.md
+++ b/doc/qos.md
@@ -2,93 +2,508 @@ Quality of Service
==================
On occasion, most networks will experience congestion due to some
-extraordinary load being placed upon it. If the load is transient,
+extraordinary load being placed upon it. If the load is transient,
switches and routers may be able to absorb such bursts of traffic by
-queuing packets in internal memories. However, if the load is
-sustained over long periods of time, queues will fill up and packets
-will start to be dropped. When such situations arise, it is the job of
-the network's Quality of Service (QoS) policy to define _which_
-packets to drop and which ones to prioritize, such that critical
-services remain operational.
+queuing packets in internal memories. However, if the load is sustained
+over long periods of time, queues will fill up and packets will start to
+be dropped. When such situations arise, it is the job of the network's
+Quality of Service (QoS) policy to define _which_ packets to drop and
+which ones to prioritize, such that critical services remain operational.
+QoS is configured per interface, under `/interfaces/interface[name='eth0']/qos/`.
+Every interface type is accepted; what the hardware can offload varies
+and is reported back in the operational datastore, see [Hardware
+Offload](#hardware-offload).
-## Hardware Forwarded Traffic
-The default QoS policy for flows which are offloaded to a switching
-ASIC is defined by the hardware defaults of the device in question.
+## Terminology
+QoS vocabulary comes from two families of standards. IEEE 802.1Q[^1]
+defines priorities and traffic classes for bridged Ethernet, the IETF
+Differentiated Services (DiffServ) RFCs define how IP packets are marked.
+
+**Priority** is the internal value, 0 to 7, that every frame carries
+through the system from classification to transmission. IEEE 802.1Q
+Annex I names a traffic type for each priority. Higher is better,
+except that priority 1 is meant for traffic that may yield to best
+effort:
+
+| Priority | Traffic type |
+|---------:|-----------------------|
+| 7 | Network Control |
+| 6 | Internetwork Control |
+| 5 | Voice |
+| 4 | Video |
+| 3 | Critical Applications |
+| 2 | Excellent Effort |
+| 1 | Background |
+| 0 | Best Effort (default) |
+/// table-caption
+IEEE 802.1Q-2022 Table I-2, traffic types.
+///
+
+**PCP and DEI** are the Priority Code Point and Drop Eligible Indicator,
+the three-bit priority and one-bit drop flag in a VLAN tag[^2]. A frame
+without a VLAN tag has neither.
+
+**DSCP** is the six-bit Differentiated Services Code Point in the IP
+header[^3]. Named codepoints group into _per-hop behaviours_ (PHB), the
+forwarding treatment a router or switch gives them[^4]:
+
+| Name | Codepoints | Reference |
+|--------------------------|--------------------------------|----------------|
+| Expedited Forwarding, EF | 46 | RFC 4594 1.5.3 |
+| Assured Forwarding, AFxy | 10..14, 18..22, 26..30, 34..38 | RFC 4594 1.5.2 |
+| Class Selector, CS1..7 | 8, 16, 24, 32, 40, 48, 56 | RFC 4594 1.5.4 |
+| Default Forwarding, DF | 0, also written CS0 | RFC 4594 1.5.1 |
+/// table-caption
+DiffServ per-hop behaviours and their codepoints.
+///
+
+The class selectors carry the old IP precedence in the top three bits,
+so CS_n_ maps naturally to priority _n_. Assured forwarding has four
+classes, AF1 to AF4, each with three drop precedences, e.g. AF11, AF12,
+AF13. RFC 4594 groups the codepoints into service classes, telephony,
+signaling, multimedia and so on, with a recommended treatment for
+each[^5].
+
+**Traffic class** is a transmit queue on the egress port. A port has 1
+to 8 of them, numbered so that the highest class is served first, and a
+table binds each of the eight priorities to one class[^6].
+
+**Transmission selection** is how the port picks the next class to send
+from[^7]. _Strict priority_ always serves the highest non-empty class.
+_Enhanced Transmission Selection_ (ETS)[^8] shares bandwidth between
+classes in proportion to a bandwidth share.
+
+**Stream Reservation (SR) classes** are the two priorities reserved for
+time-sensitive audio and video streams in AVB and TSN networks[^9]:
+SR class A on priority 3 and SR class B on priority 2.
+
+**DCB**, Data Center Bridging[^10], is the Linux kernel interface through
+which per-port priority tables are programmed into switch hardware.
+
+
+## The Pipeline
+
+{ width=460 }
+/// figure-caption
+The QoS pipeline: ingress climbs to forwarding, egress descends to the wire.
+///
+
+A frame passes six stages, three on the port it arrives on and three on
+the port it leaves by:
+
+1. **Classify** assigns the priority from the PCP of the VLAN tag or the
+ DSCP of the IP header, depending on what the port trusts. Frames
+ matching neither get the port's default priority.
+2. **Match flows** may override the classification for traffic picked out
+ by, e.g., source address and port.
+3. **Police and gate** limits the rate of a port, of broadcast and
+ multicast, or of an individual stream.
+4. **Map to traffic class** on the egress port binds the eight priorities
+ to the port's traffic classes.
+5. **Transmission selection** decides which class transmits next.
+6. **Egress processing** rewrites PCP and DSCP from the priority and caps
+ the port rate.
+
+Stages 1, 4, 5 and 6 are configurable and described below. Stages 2
+and 3 have no settings under `qos`.
+
+
+## Ingress Classification
+
+Configured under `qos ingress`:
+
+| Setting | Description |
+|--------------------|------------------------------------------------|
+| `trust` | Trusted fields in order, default `pcp` |
+| `default-priority` | Priority when no trusted field matches |
+| `pcp-map` | PCP to priority, preset or custom table |
+| `dscp-map` | DSCP to priority, preset or custom table |
+/// table-caption
+Ingress classification settings.
+///
+
+The first trusted field that yields a result wins, and a field only
+yields a result for frames that carry it. So the five `trust` values
+differ on frames that have one field but not the other:
+
+| Frame \ trust | `pcp` | `pcp-dscp` | `dscp` | `dscp-pcp` | `none` |
+|-----------------|---------|------------|---------|------------|---------|
+| Tagged IP | PCP | PCP | DSCP | DSCP | default |
+| Tagged, not IP | PCP | PCP | default | PCP | default |
+| Untagged IP | default | DSCP | DSCP | DSCP | default |
+| Untagged, other | default | default | default | default | default |
+/// table-caption
+Which field sets the priority, per frame type and trust setting.
+///
+
+"default" is `default-priority`, 0 unless set. The default `trust` is
+`pcp`: a bridge trusts the tag, as IEEE 802.1Q has it, and classification
+is on out of the box for tagged traffic.
+
+The usual arrangement classifies once, where traffic enters the network:
+edge ports are set to `dscp-pcp` or `dscp` and remark both fields from
+the result, see [Egress Remarking](#egress-remarking). Every switch
+behind them keeps the default and carries the edge's decision through.
+
+A custom `pcp-map` is keyed by PCP and DEI. A custom `dscp-map` is keyed
+by DSCP with a `trusted` flag per entry, so individual codepoints can be
+believed while the rest fall through to the default priority.
+
+### Presets
+
+| Preset | Map | Mapping |
+|--------|------|--------------------------------------------------|
+| `ieee` | PCP | 802.1Q default decoding, PCP _n_ to priority _n_ |
+| `ietf` | DSCP | RFC 4594 service classes |
+| `msb` | DSCP | Top three bits of the DSCP, CS_n_ to _n_ |
+/// table-caption
+Classification presets.
+///
+
+Both maps default to their first preset. The `ietf` preset follows the
+RFC 4594 service classes[^5]; codepoints not listed fall through to the
+default priority:
+
+| Service class | DSCP | ⇒ | Priority |
+|---------------------------------|------------|---|---------:|
+| Network control | CS6, CS7 | ⇒ | 6, 7 |
+| Telephony, signaling | EF, CS5 | ⇒ | 5 |
+| Real-time and conferencing | CS4, AF4x | ⇒ | 4 |
+| Streaming and broadcast video | CS3, AF3x | ⇒ | 3 |
+| Low-latency data, OAM | CS2, AF2x | ⇒ | 2 |
+| High-throughput, low-priority | CS1, AF1x | ⇒ | 1 |
+| Standard | CS0 | ⇒ | 0 |
+/// table-caption
+The `ietf` DSCP preset.
+///
+
+### Examples
+
+A trunk port trusting DSCP first, then PCP, with the standard maps:
+
+
admin@example:/config/> edit interface e1 qos ingress
+admin@example:/config/interface/…/ingress/> set trust dscp-pcp
+admin@example:/config/interface/…/ingress/> set default-priority 0
+admin@example:/config/interface/…/ingress/> set dscp-map preset ietf
+admin@example:/config/interface/…/ingress/> set pcp-map preset ieee
+admin@example:/config/interface/…/ingress/> leave
+
+
+An access port that believes only EF (46) and AF31 (26) and gives
+everything else the port default:
+
+
admin@example:/config/> edit interface e2 qos ingress
+admin@example:/config/interface/…/ingress/> set trust dscp
+admin@example:/config/interface/…/ingress/> set default-priority 0
+admin@example:/config/interface/…/ingress/> set dscp-map entry 46 priority 5
+admin@example:/config/interface/…/ingress/> set dscp-map entry 26 priority 3
+admin@example:/config/interface/…/ingress/> leave
+
+
+Which trust orders a port accepts is hardware dependent and listed in
+`qos capabilities supported-trust-order`. An order the port cannot
+honour is rejected. On ports whose driver has no ingress classification
+support the same rules are applied by the kernel to the traffic it
+forwards, see [Hardware Offload](#hardware-offload).
+
+
+## Traffic Classes and Transmission Selection
+
+Configured under `qos egress`:
+
+| Setting | Description |
+|------------------------------|--------------------------------------|
+| `preset` | `ieee` (default) or `ieee-sr` |
+| `priority0` .. `priority7` | Custom traffic class per priority |
+| `traffic-class ID algorithm` | Transmission selection algorithm |
+| `traffic-class ID bandwidth` | Share of a weighted class, percent |
+/// table-caption
+Traffic class settings.
+///
+
+The first two live under `traffic-class-table`. The number of classes
+is not configuration: a port has one class per transmit queue, at most
+eight, and a single-queue port has no queue structure to respect and gets
+all eight. The count is reported as `max-traffic-classes` and picks the
+column of the preset. The mapping is a preset, `ieee` for an ordinary
+bridge or `ieee-sr` for ports carrying reserved streams, both described
+below, or a custom table where an unset priority falls back to the `ieee`
+value. The algorithm is `strict-priority` (default) or
+`enhanced-transmission-selection`, the latter sharing what the strict
+classes leave in proportion to `bandwidth`, a percentage the weighted
+classes of a port sum to 100 with. The scheduler is work-conserving, so
+the shares are relative and a class that has nothing to send lends its
+share to the others.
+
+Strict-priority classes must be the highest-numbered ones, with the
+weighted classes below them; other layouts are rejected.
+
+A port with four queues, the top two classes strict, the bottom two
+sharing what is left in a 2:1 ratio. No map is set, so the `ieee`
+preset supplies Table 8-5's four-class column, `0 0 1 1 2 2 3 3`:
+
+
admin@example:/config/> edit interface e1 qos egress
+admin@example:/config/interface/…/egress/> set traffic-class 3 algorithm strict-priority
+admin@example:/config/interface/…/egress/> set traffic-class 2 algorithm strict-priority
+admin@example:/config/interface/…/egress/> set traffic-class 1 algorithm enhanced-transmission-selection
+admin@example:/config/interface/…/egress/> set traffic-class 1 bandwidth 67
+admin@example:/config/interface/…/egress/> set traffic-class 0 algorithm enhanced-transmission-selection
+admin@example:/config/interface/…/egress/> set traffic-class 0 bandwidth 33
+admin@example:/config/interface/…/egress/> leave
+
+
+
+### Rate Limit
+
+A port can be held below its link speed with one token bucket on the
+whole port, configured under `qos egress rate-limit`. `rate` is the
+sustained rate in bits per second and `burst` how many bytes may leave
+at once from a full bucket; left unset, the burst covers ten
+milliseconds at the configured rate, never less than one maximum-size
+frame. The bucket sits above the traffic classes: it bounds what the
+port sends in total, and transmission selection decides which class
+gets the bounded rate.
+
+
+
+On a port without offload the limit applies to the frames the CPU
+sends. Where the driver takes the bucket it applies to the frames the
+fabric forwards as well, and `offload` under `capabilities` then lists
+`rate-limit`.
+
+
+### Egress Remarking
+
+Configured under `qos egress remark`, both leaves default to `none`:
+
+| Setting | Description |
+|---------|--------------------------------------------------|
+| `pcp` | `from-priority` writes PCP on transmit |
+| `dscp` | `from-priority` writes DSCP on transmit |
+/// table-caption
+Remarking settings.
+///
+
+PCP is set to the priority with DEI cleared, DSCP to the class selector
+with the same number, CS0 to CS7. Together with a trust order on the
+receiving port, a downstream device then sees this device's
+classification rather than the sender's marking. With `none` nothing is
+rewritten by configuration, and what a tagged frame leaves with depends
+on the path it took: frames the kernel forwards keep the PCP they
+arrived with, while a switch fabric encodes the PCP from the frame's
+priority, as an IEEE 802.1Q bridge does[^12]. With the default
+`pcp-map` the two are the same. To remark both fields from the
+priority:
+
+
admin@example:/config/> edit interface e1 qos egress remark
+admin@example:/config/interface/…/remark/> set pcp from-priority
+admin@example:/config/interface/…/remark/> set dscp from-priority
+admin@example:/config/interface/…/remark/> leave
+
+
+Remarking uses the same driver support as ingress classification, see
+[Hardware Offload](#hardware-offload). Without it, DSCP is rewritten by
+the kernel for the traffic it forwards. PCP is not: the kernel cannot
+change a tag's priority without also setting its VLAN ID, so on a port
+without driver support, PCP follows priority only where the tag is
+created, on VLAN interfaces with `egress-qos pcp from-priority`, see
+[VLAN Interfaces](#vlan-interfaces).
+
+
+### Defaults
+
+An interface without `qos` configuration is fully specified by the
+defaults, and every physical port runs them from boot: trust PCP with
+the `ieee` preset, default priority 0 for untagged frames, one traffic
+class per queue with the `ieee` preset, IEEE 802.1Q-2022 Table 8-5, the
+standard's recommendation for ordinary bridges, and strict priority
+throughout. Columns are the number of traffic classes on the port:
+
+| Priority \ #TC | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+|:---------------|--:|--:|--:|--:|--:|--:|--:|
+| 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
+| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+| 2 | 0 | 0 | 1 | 1 | 2 | 2 | 2 |
+| 3 | 0 | 0 | 1 | 1 | 2 | 3 | 3 |
+| 4 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
+| 5 | 1 | 1 | 2 | 2 | 3 | 4 | 5 |
+| 6 | 1 | 2 | 3 | 3 | 4 | 5 | 6 |
+| 7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+/// table-caption
+IEEE 802.1Q-2022 Table 8-5, recommended priority to traffic class mappings.
+///
+
+The factory configuration carries no `qos` settings; removing a port's
+`qos` container returns it to these defaults. Virtual interfaces,
+bridges, VLANs and the like, get a pipeline only when configured.
+
+
+### Stream Reservation Layout
+
+The `ieee-sr` preset is IEEE 802.1Q-2022 Table 34-1, the recommended
+mapping for ports carrying reserved streams. The SR classes, priority 3
+(class A) and priority 2 (class B), map to the _highest_ traffic classes
+so they outrank everything else at transmission selection, with best
+effort below:
+
+| Priority \ #TC | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+|:---------------|--:|--:|--:|--:|--:|--:|--:|
+| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+| 2 (SR B) | 1 | 1 | 2 | 3 | 4 | 5 | 6 |
+| 3 (SR A) | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+| 4 | 0 | 0 | 1 | 1 | 1 | 1 | 2 |
+| 5 | 0 | 0 | 1 | 1 | 1 | 2 | 3 |
+| 6 | 0 | 0 | 1 | 2 | 2 | 3 | 4 |
+| 7 | 0 | 0 | 1 | 2 | 3 | 4 | 5 |
+/// table-caption
+IEEE 802.1Q-2022 Table 34-1, priority to traffic class mappings with SR classes.
+///
+
+Apply it on ports where reserved streams are expected:
+
+
+
+
+## Hardware Offload
+
+The system runs on a wide range of hardware, and offload is best effort.
+The same configuration is accepted everywhere; where it ends up differs,
+and each port reports it under `qos capabilities`:
+
+| Capability | Description |
+|-------------------------|-------------------------------------------------|
+| `max-traffic-classes` | Traffic classes on the port, eight unless shown |
+| `supported-trust-order` | `trust` values the driver accepts |
+| `offload` | Stages the driver runs in hardware |
+/// table-caption
+QoS capabilities a port reports.
+///
+
+`supported-trust-order` is absent when the driver has no ingress
+classification support. `offload` lists `classification`, `remarking`, `transmission-selection`
+and `rate-limit` as the driver takes them; a stage not listed runs in
+the kernel.
+
+Each feature maps to one Linux mechanism, and whether it reaches the
+hardware depends on the driver implementing the matching hook:
+
+| Feature | Linux tool | Driver hook | Fallback |
+|------------------------|----------------|----------------------|-----------------------|
+| Ingress classification | `dcb app` | `dcbnl` app ops | `tc flower`, software |
+| Trust order | `dcb apptrust` | `dcbnl_setapptrust` | Rule order, software |
+| Egress remarking | `dcb rewr` | `dcbnl_setrewr` | DSCP only, software |
+| Transmission selection | `tc ets` | `TC_SETUP_QDISC_ETS` | software |
+| Port rate limit | `tc tbf` | `TC_SETUP_QDISC_TBF` | software |
+/// table-caption
+QoS features and their Linux backends.
+///
+
+"Software" means the kernel does the work for every frame the CPU
+handles on the port: `tc flower` rules classify what arrives, the `ets`
+qdisc schedules what leaves and `pedit` rewrites its DSCP, for locally
+originated, routed, and software-bridged traffic alike. On a NIC-based
+device that is all traffic. On a switch it excludes frames the fabric
+forwards port to port without the CPU, so there it covers routed
+traffic, traffic to and from the device itself, and bridging between
+ports in different switch domains.
+
+On a switch whose driver lacks DCB the fabric keeps classifying
+port-to-port traffic by its own defaults while the kernel classifies the
+CPU path per configuration. PCP remarking has no software counterpart;
+where the driver lacks it the setting is accepted and noted in the system
+log.
### Marvell LinkStreet
-This family of devices, sometimes also referred to as _SOHO_, are
-managed by the `mv88e6xxx` driver in the Linux kernel. While older
-chips in this family where limited to 4 output queues per port, this
-documentation is _only_ valid for newer generations with 8 output
-queues per port.
+This family of switch chips is managed by the `mv88e6xxx` driver in the
+Linux kernel. The system carries patches that expose the per-port
+classification and remarking tables of the 88E6390 and 88E6393X
+generations through DCB, so ingress classification, remarking and the
+traffic class table are all offloaded on these chips. This section is
+_only_ valid for generations with 8 output queues per port.
-
+{ width=600 }
+/// figure-caption
+Hardware offloading for Marvell LinkStreet.
+///
The picture illustrates packets having their priority determined at
-ingress, here interface _e1_ and _e3_. In this example, both packets
+ingress, here interface _e1_ and _e3_. In this example, both packets
are forwarded to the same outgoing interface (_e2_), subject to output
-queueing. The sections below provides more information on these
-topics.
-
-#### Default Policy
-
-##### Queueing
-
-Both layer 2 ([VLAN PCP][2]) and layer 3 ([IP DSCP][3]) priority marks
-are considered when selecting the output queue of an incoming
-frame. PCP to queue mapping is done 1:1. For IP packets, the 3 most
-significant bits of the DSCP is used to select the queue:
-
-| PCP | DSCP | ⇒ | Queue | Weight |
-|----:|------:|---|------:|-------:|
-| 0 | 0-7 | ⇒ | 0 | 1 |
-| 1 | 8-15 | ⇒ | 1 | 2 |
-| 2 | 16-23 | ⇒ | 2 | 3 |
-| 3 | 24-31 | ⇒ | 3 | 6 |
-| 4 | 32-39 | ⇒ | 4 | 12 |
-| 5 | 40-47 | ⇒ | 5 | 17 |
-| 6 | 48-55 | ⇒ | 6 | 25 |
-| 7 | 56-63 | ⇒ | 7 | 33 |
-
-For packets containing both a VLAN tag and an IP header, PCP priority
-takes precedence over DSCP priority. In cases where neither are
-available, packets are always assigned to queue 0.
-
-Each port's set of 8 egress queues operate on a Weighted Round Robin
-([WRR][4]) schedule, using the weights listed in the table above. The
-sum of all weights adds up to 99, meaning that the weight of any given
-queue is roughly equivalent to the percentage of the available
-bandwidth reserved for it.
-
-##### Marking
-
-Any priority marks available on ingress are left unmodified when the
-frame egresses an output port. In the case when an IP packet ingresses
-_without_ a VLAN tag, and is to egress _with_ a VLAN tag, its PCP is
-set to the 3 most significant bits of it. If no priority information
-is available in the frame on ingress (i.e. untagged non-IP), then
-packets will egress out of tagged ports with PCP set to 0.
+queueing.
-## Software Forwarded Traffic
+Each port has its own PCP and DSCP tables, so the `pcp-map`, `dscp-map`
+and `default-priority` settings apply as configured, and all four trust
+orders are accepted. Three hardware details show through:
+
+- A frame that is both VLAN-tagged and IP always takes its _frame_
+ priority, the value written back as PCP on egress, from the tag. The
+ trust order `dscp-pcp` decides only which field selects the output
+ queue.
+- The PCP of every tagged frame encodes the frame's priority, on one
+ chip as across a cascade of chips, which only carry the priority
+ between them. The `remark pcp` setting therefore changes nothing on
+ these switches; the DEI comes from the frame's color, never from a
+ table.
+- Frames the CPU itself sends, routed or locally originated, are injected
+ past the tables, so their DSCP is remarked by the kernel instead and
+ their PCP comes from the VLAN interface settings described below.
+
+The `traffic-class-table` and the `traffic-class` list apply to hardware
+forwarded frames as well: each priority is queued in its traffic class,
+the strict classes are served first, and the weighted classes share the
+rest by Weighted Round Robin (WRR)[^11] with the `bandwidth` shares as
+weights. The `rate-limit` is the port's own egress shaper. Four
+hardware details show through:
+
+- A frame's output queue is chosen where it enters the switch, by the
+ ingress port's tables, so the `traffic-class-table` is one per switch
+ chip, not per port. The port configured most recently defines it,
+ and every port whose own table differs is no longer offloaded: its
+ `offload` list under `capabilities` leaves out
+ `transmission-selection`. Give every port the same table.
+- The WRR weights are one set per switch chip as well, defined the
+ same way by the port configured most recently for each weighted
+ class. Give every port with weighted classes the same shares.
+- A frame crossing from one chip to another inside a device carries its
+ priority between them, as it would between two switches in a network,
+ and the far chip queues it in the class of that priority. This is the
+ general rule of QoS in a network: classification at the first ingress
+ decides the priority, and every hop after it trusts and queues by that
+ priority. Steer traffic with the ingress `dscp-map` and `pcp-map`;
+ a `traffic-class-table` other than the identity holds for frames that
+ enter and leave on the same chip.
+- The WRR counts frames where `bandwidth` speaks of bytes, so the shares
+ hold for traffic of similar frame sizes.
-For packets which are processed by a CPU, i.e. typically routed
-traffic, and bridged traffic between interfaces that do not belong to
-the same hardware switching domain, an [nftables container][1] can be
-used to define a QoS policy.
+Without any `qos` configuration the class table is IEEE 802.1Q-2022
+Table 8-5 with every class strict, as on any other port.
-For VLAN interfaces, Infix provides support for mapping the Priority
-Code Point (PCP) to internal priority on ingress, and the reverse on
-egress.
-
+## VLAN Interfaces
+For VLAN interfaces, the system supports mapping the PCP to internal
+priority on ingress, and the reverse on egress. This is separate from
+the `qos` settings above, which on a VLAN interface govern only its
+traffic classes.
+
+{ width=600 }
+/// figure-caption
+Ingress and egress priority mapping for VLAN interfaces.
+///
These `ingress-qos` and `egress-qos` settings are done per VLAN, both
-defaulting to '0'. The example below shows how to keep the PCP priority
+defaulting to '0'. The example below shows how to keep the PCP priority
for packets being routed between two VLAN interfaces.
-## A complex example
-The picture below shows a packet flow being subject both to software
-forwarding and hardware offloading.
+## Software Forwarded Traffic
+
+For packets which are processed by a CPU, i.e. typically routed traffic,
+and bridged traffic between interfaces that do not belong to the same
+hardware switching domain, the traffic class table and transmission
+selection above apply in software. For classification and marking
+beyond what the `qos` settings offer, an [nftables container][nft] can
+be used to define a QoS policy.
+
+The picture below shows a packet flow subject to both: classified and
+queued by the switch fabric on the way in and out, and carrying its
+priority through the VLAN interfaces and a container with a traffic
+control policy in between.
+
+{ width=600 }
+/// figure-caption
+Hardware and software QoS handling.
+///
-
+[nft]: container.md#application-container-nftables
-[1]: container.md#application-container-nftables
-[2]: https://en.wikipedia.org/wiki/IEEE_802.1Q
-[3]: https://en.wikipedia.org/wiki/Differentiated_services
-[4]: https://en.wikipedia.org/wiki/Weighted_round_robin
+[^1]: IEEE Std 802.1Q-2022, Bridges and Bridged Networks,
+ , also
+
+[^2]:
+[^3]: RFC 2474, Definition of the Differentiated Services Field,
+
+[^4]: RFC 4594, Configuration Guidelines for DiffServ Service Classes,
+ sections 1.4.5 and 1.5,
+[^5]: RFC 4594, section 2.3, Service Class Characteristics,
+
+[^6]: IEEE Std 802.1Q-2022, clause 8.6.6, Queuing frames
+[^7]: IEEE Std 802.1Q-2022, clause 8.6.8, Transmission selection
+[^8]:
+[^9]:
+[^10]: , and
+ the `dcb(8)` manual page
+[^11]:
+[^12]: Clause 6.9.3 of IEEE Std 802.1Q-2022, the PCP encoding table.
+ The received PCP is only kept because the default tables decode
+ and encode it to itself; once classification changes the
+ priority, the transmitted PCP follows.
diff --git a/doc/vpn-wireguard.md b/doc/vpn-wireguard.md
index 5b3971108e..7f42cf6f32 100644
--- a/doc/vpn-wireguard.md
+++ b/doc/vpn-wireguard.md
@@ -113,7 +113,9 @@ The `allowed-ips` setting in WireGuard serves two critical purposes:
Think of `allowed-ips` as a combination of firewall rules and routing table.

-*Figure: WireGuard key exchange and allowed-ips configuration between two peers*
+/// figure-caption
+WireGuard key exchange and allowed-ips configuration between two peers.
+///
For a simple point-to-point tunnel, you typically allow only the peer's
tunnel IP address (e.g., `10.0.0.2/32`). For site-to-site VPNs connecting
@@ -204,7 +206,9 @@ specify per-peer differences.
## Site-to-Site VPN

-*Figure: Site-to-Site VPN connecting two office networks*
+/// figure-caption
+Site-to-Site VPN connecting two office networks.
+///
A site-to-site VPN connects entire networks across locations, creating a unified
private network over the internet. This allows devices in one location to
@@ -218,7 +222,9 @@ then add the remote network to `allowed-ips` and configure static routes.
## Road Warrior VPN

-*Figure: Mobile clients connecting to corporate network*
+/// figure-caption
+Mobile clients connecting to corporate network.
+///
For mobile clients or peers without fixed IPs, omit the `endpoint` setting.
WireGuard learns the peer's endpoint from authenticated incoming packets:
@@ -236,7 +242,9 @@ and tracks the mobile client's changing IP address automatically.
## Hub-and-Spoke Topology

-*Figure: Hub-and-Spoke topology with central hub routing traffic between spokes*
+/// figure-caption
+Hub-and-Spoke topology with central hub routing traffic between spokes.
+///
WireGuard excels at hub-and-spoke (star) topologies where multiple remote
sites connect to a central hub.
diff --git a/doc/vpn.md b/doc/vpn.md
index d17456c2e8..73c60f05cc 100644
--- a/doc/vpn.md
+++ b/doc/vpn.md
@@ -33,7 +33,9 @@ VPNs are typically deployed in one of several models:
**Site-to-Site VPN**

-*Figure: Site-to-Site VPN connecting two office networks*
+/// figure-caption
+Site-to-Site VPN connecting two office networks.
+///
Connects entire networks across locations, creating a unified private network
over the internet. Routers or firewalls at each site maintain persistent
@@ -46,7 +48,9 @@ tunnels, allowing seamless access between locations.
**Remote Access VPN (Road Warrior)**

-*Figure: Mobile clients connecting to corporate network*
+/// figure-caption
+Mobile clients connecting to corporate network.
+///
Enables individual users to securely access a private network from remote
locations. Clients initiate connections as needed from dynamic IP addresses.
@@ -58,7 +62,9 @@ locations. Clients initiate connections as needed from dynamic IP addresses.
**Hub-and-Spoke VPN**

-*Figure: Hub-and-Spoke topology with central hub routing traffic between spokes*
+/// figure-caption
+Hub-and-Spoke topology with central hub routing traffic between spokes.
+///
A central hub connects to multiple remote sites (spokes), routing traffic
between them. Spokes don't connect directly to each other but communicate
diff --git a/mkdocs.yml b/mkdocs.yml
index 435cc45c66..95e29fd320 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,6 +8,8 @@ docs_dir: doc/
edit_uri: edit/master/doc/
extra_css:
- extra.css
+extra_javascript:
+ - print-keep.js
exclude_docs: |
ChangeLog.md
@@ -131,7 +133,14 @@ markdown_extensions:
- attr_list
- footnotes
- md_in_html
- - pymdownx.blocks.caption
+ - pymdownx.blocks.caption:
+ auto: true
+ types:
+ - name: caption
+ - name: figure-caption
+ prefix: 'Figure {}:'
+ - name: table-caption
+ prefix: 'Table {}:'
- pymdownx.critic
- pymdownx.caret
- pymdownx.keys
diff --git a/patches/linux/6.18.59/0134-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch b/patches/linux/6.18.59/0134-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch
new file mode 100644
index 0000000000..dfe4bf1131
--- /dev/null
+++ b/patches/linux/6.18.59/0134-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch
@@ -0,0 +1,733 @@
+From 916edb2eca0e37dd8dec272d3dfe0b172112f453 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:10:33 +0200
+Subject: [PATCH 134/139] net: dsa: mv88e6xxx: Add DCB priority classification
+Organization: Wires
+
+The 6390 family classifies frames per port from the tag PCP through the
+IEEE Priority Mapping Table, from the DSCP through the IP Priority
+Mapping Table, and falls back to the port default priority. Which
+sources are trusted, and which wins when a frame is both tagged and
+IP, is set by InitialPri and TagIfBoth in Port Control 0. None of it
+is reachable from userspace, the driver hardcodes identity mappings
+and trusts both sources at setup.
+
+Expose the tables and the trust order through the DSA DCB ops:
+
+ dcb app add dev lan1 default-prio 2 pcp-prio 5nd:5 dscp-prio 46:5
+ dcb apptrust set dev lan1 order dscp pcp
+
+A deleted entry has its Dis*Pri bits set so the port default applies,
+and is skipped when the table is read back at init. The frame and
+queue priorities are always set to the same value. When a frame is
+both tagged and IP the frame priority comes from the tag regardless,
+TagIfBoth only selects the source of the queue priority.
+
+Signed-off-by: Joachim Wiberg
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 217 +++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/chip.h | 15 ++
+ drivers/net/dsa/mv88e6xxx/port.c | 265 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/port.h | 42 +++++
+ 4 files changed, 539 insertions(+)
+
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
+index 54bf531fcc35..139c3cda1079 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4855,6 +4855,12 @@ static const struct mv88e6xxx_ops mv88e6190_ops = {
+ .port_set_speed_duplex = mv88e6390_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -4915,6 +4921,12 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = {
+ .port_set_speed_duplex = mv88e6390x_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390x_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -4975,6 +4987,12 @@ static const struct mv88e6xxx_ops mv88e6191_ops = {
+ .port_set_speed_duplex = mv88e6390_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+ .port_set_mcast_flood = mv88e6352_port_set_mcast_flood,
+@@ -5139,6 +5157,12 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
+ .port_set_speed_duplex = mv88e6390_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5530,6 +5554,12 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
+ .port_set_speed_duplex = mv88e6390_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5594,6 +5624,12 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = {
+ .port_set_speed_duplex = mv88e6390x_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6390x_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5657,6 +5693,12 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
+ .port_set_speed_duplex = mv88e6393x_port_set_speed_duplex,
+ .port_max_speed_mode = mv88e6393x_port_max_speed_mode,
+ .port_tag_remap = mv88e6390_port_tag_remap,
++ .port_get_default_prio = mv88e6390_port_get_default_prio,
++ .port_set_default_prio = mv88e6390_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio,
++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_set_policy = mv88e6393x_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -7166,6 +7208,171 @@ static int mv88e6xxx_crosschip_lag_leave(struct dsa_switch *ds, int sw_index,
+ return err_sync ? : err_pvt;
+ }
+
++static int mv88e6xxx_port_get_default_prio(struct dsa_switch *ds, int port)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_get_default_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_default_prio(chip, port);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_set_default_prio(struct dsa_switch *ds, int port,
++ u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_default_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_default_prio(chip, port, prio);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_get_dscp_prio(struct dsa_switch *ds, int port,
++ u8 dscp)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_get_dscp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_dscp_prio(chip, port, dscp);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_add_dscp_prio(struct dsa_switch *ds, int port,
++ u8 dscp, u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_dscp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_dscp_prio(chip, port, dscp, prio);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++/* Only clear the entry if it still holds the priority being deleted,
++ * another APP entry for the same code point may have taken it over.
++ */
++static int mv88e6xxx_port_del_dscp_prio(struct dsa_switch *ds, int port,
++ u8 dscp, u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_dscp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_dscp_prio(chip, port, dscp);
++ if (err == prio)
++ err = chip->info->ops->port_set_dscp_prio(chip, port, dscp, -1);
++ else if (err >= 0 || err == -EOPNOTSUPP)
++ err = 0;
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_get_pcp_prio(struct dsa_switch *ds, int port,
++ u8 pcp, u8 dei)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_get_pcp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_pcp_prio(chip, port, pcp, dei);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_add_pcp_prio(struct dsa_switch *ds, int port,
++ u8 pcp, u8 dei, u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_pcp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_pcp_prio(chip, port, pcp, dei, prio);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_del_pcp_prio(struct dsa_switch *ds, int port,
++ u8 pcp, u8 dei, u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_pcp_prio)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_pcp_prio(chip, port, pcp, dei);
++ if (err == prio)
++ err = chip->info->ops->port_set_pcp_prio(chip, port, pcp, dei,
++ -1);
++ else if (err >= 0 || err == -EOPNOTSUPP)
++ err = 0;
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_dcb_set_apptrust(struct dsa_switch *ds, int port,
++ const u8 *sel, int nsel)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ mv88e6xxx_reg_lock(chip);
++ err = mv88e6xxx_port_set_apptrust(chip, port, sel, nsel);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_dcb_get_apptrust(struct dsa_switch *ds, int port,
++ u8 *sel, int *nsel)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ mv88e6xxx_reg_lock(chip);
++ err = mv88e6xxx_port_get_apptrust(chip, port, sel, nsel);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
+ static int mv88e6xxx_port_add_etype_prio(struct dsa_switch *ds, int port,
+ u16 etype, u8 prio)
+ {
+@@ -7352,6 +7559,16 @@ static const struct dsa_switch_ops mv88e6xxx_switch_ops = {
+ .crosschip_lag_change = mv88e6xxx_crosschip_lag_change,
+ .crosschip_lag_join = mv88e6xxx_crosschip_lag_join,
+ .crosschip_lag_leave = mv88e6xxx_crosschip_lag_leave,
++ .port_get_default_prio = mv88e6xxx_port_get_default_prio,
++ .port_set_default_prio = mv88e6xxx_port_set_default_prio,
++ .port_get_dscp_prio = mv88e6xxx_port_get_dscp_prio,
++ .port_add_dscp_prio = mv88e6xxx_port_add_dscp_prio,
++ .port_del_dscp_prio = mv88e6xxx_port_del_dscp_prio,
++ .port_get_pcp_prio = mv88e6xxx_port_get_pcp_prio,
++ .port_add_pcp_prio = mv88e6xxx_port_add_pcp_prio,
++ .port_del_pcp_prio = mv88e6xxx_port_del_pcp_prio,
++ .port_set_apptrust = mv88e6xxx_dcb_set_apptrust,
++ .port_get_apptrust = mv88e6xxx_dcb_get_apptrust,
+ .port_add_etype_prio = mv88e6xxx_port_add_etype_prio,
+ .port_del_etype_prio = mv88e6xxx_port_del_etype_prio,
+ .port_setup_tc = mv88e6xxx_port_setup_tc,
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
+index 5d5e7ab63fca..bb5739cf8cf9 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.h
++++ b/drivers/net/dsa/mv88e6xxx/chip.h
+@@ -576,6 +576,21 @@ struct mv88e6xxx_ops {
+
+ int (*port_tag_remap)(struct mv88e6xxx_chip *chip, int port);
+
++ /* Per-port priority classification, a negative prio disables
++ * the entry so the port default applies.
++ */
++ int (*port_get_default_prio)(struct mv88e6xxx_chip *chip, int port);
++ int (*port_set_default_prio)(struct mv88e6xxx_chip *chip, int port,
++ u8 prio);
++ int (*port_get_dscp_prio)(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp);
++ int (*port_set_dscp_prio)(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp, int prio);
++ int (*port_get_pcp_prio)(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei);
++ int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei, int prio);
++
+ int (*port_set_policy)(struct mv88e6xxx_chip *chip, int port,
+ enum mv88e6xxx_policy_mapping mapping,
+ enum mv88e6xxx_policy_action action);
+diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
+index 843972077576..f7991eeb9d17 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.c
++++ b/drivers/net/dsa/mv88e6xxx/port.c
+@@ -9,6 +9,7 @@
+ */
+
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -1096,6 +1097,83 @@ int mv88e6xxx_port_set_fid(struct mv88e6xxx_chip *chip, int port, u16 fid)
+ return 0;
+ }
+
++/* Offset 0x04: Port Control 0, InitialPri and TagIfBoth
++ *
++ * When a frame is both tagged and IP, and both sources are trusted, the
++ * frame priority (FPri) always comes from the tag; TagIfBoth only picks
++ * the source of the queue priority (QPri) and color.
++ */
++
++int mv88e6xxx_port_set_apptrust(struct mv88e6xxx_chip *chip, int port,
++ const u8 *sel, int nsel)
++{
++ u16 reg, bits = 0;
++ int i, err;
++
++ if (nsel > 2)
++ goto invalid;
++
++ for (i = 0; i < nsel; i++) {
++ switch (sel[i]) {
++ case DCB_APP_SEL_PCP:
++ if (bits & MV88E6185_PORT_CTL0_USE_TAG)
++ goto invalid;
++ bits |= MV88E6185_PORT_CTL0_USE_TAG;
++ if (i == 0 && nsel == 2)
++ bits |= MV88E6XXX_PORT_CTL0_TAG_IF_BOTH;
++ break;
++ case IEEE_8021QAZ_APP_SEL_DSCP:
++ if (bits & MV88E6185_PORT_CTL0_USE_IP)
++ goto invalid;
++ bits |= MV88E6185_PORT_CTL0_USE_IP;
++ break;
++ default:
++ goto invalid;
++ }
++ }
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL0, ®);
++ if (err)
++ return err;
++
++ reg &= ~(MV88E6185_PORT_CTL0_USE_TAG | MV88E6185_PORT_CTL0_USE_IP |
++ MV88E6XXX_PORT_CTL0_TAG_IF_BOTH);
++ reg |= bits;
++
++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL0, reg);
++
++invalid:
++ dev_err(chip->dev, "p%d: supported trust orders: pcp, dscp, pcp dscp, dscp pcp\n",
++ port);
++ return -EINVAL;
++}
++
++int mv88e6xxx_port_get_apptrust(struct mv88e6xxx_chip *chip, int port,
++ u8 *sel, int *nsel)
++{
++ u16 reg;
++ int err;
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL0, ®);
++ if (err)
++ return err;
++
++ *nsel = 0;
++
++ if (reg & MV88E6185_PORT_CTL0_USE_TAG &&
++ reg & MV88E6XXX_PORT_CTL0_TAG_IF_BOTH)
++ sel[(*nsel)++] = DCB_APP_SEL_PCP;
++
++ if (reg & MV88E6185_PORT_CTL0_USE_IP)
++ sel[(*nsel)++] = IEEE_8021QAZ_APP_SEL_DSCP;
++
++ if (reg & MV88E6185_PORT_CTL0_USE_TAG &&
++ !(reg & MV88E6XXX_PORT_CTL0_TAG_IF_BOTH))
++ sel[(*nsel)++] = DCB_APP_SEL_PCP;
++
++ return 0;
++}
++
+ /* Offset 0x07: Default Port VLAN ID & Priority */
+
+ int mv88e6xxx_port_get_pvid(struct mv88e6xxx_chip *chip, int port, u16 *pvid)
+@@ -1136,6 +1214,51 @@ int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid)
+ return 0;
+ }
+
++/* The default frame priority (FPri) lives here, the default queue
++ * priority (QPri) in Port Control 2. Both are set to the same value.
++ */
++int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port)
++{
++ u16 reg;
++ int err;
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN,
++ ®);
++ if (err)
++ return err;
++
++ return FIELD_GET(MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK, reg);
++}
++
++int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 prio)
++{
++ u16 reg;
++ int err;
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN,
++ ®);
++ if (err)
++ return err;
++
++ reg &= ~MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK;
++ reg |= FIELD_PREP(MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK, prio);
++
++ err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN,
++ reg);
++ if (err)
++ return err;
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL2, ®);
++ if (err)
++ return err;
++
++ reg &= ~MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK;
++ reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK, prio);
++
++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL2, reg);
++}
++
+ /* Offset 0x08: Port Control 2 Register */
+
+ static const char * const mv88e6xxx_port_8021q_mode_names[] = {
+@@ -1647,6 +1770,80 @@ int mv88e6393x_port_led_read(struct mv88e6xxx_chip *chip, int port,
+ return 0;
+ }
+
++/* Offset 0x17: IP Priority Mapping Table
++ *
++ * One entry per DSCP, holding the frame and queue priority assigned
++ * to IPv4 and IPv6 frames. A disabled entry leaves the port default.
++ */
++
++static int mv88e6390_port_ippmt_write(struct mv88e6xxx_chip *chip, int port,
++ u8 ptr, u16 data)
++{
++ u16 reg;
++
++ reg = MV88E6390_PORT_IP_PRIO_MAP_TABLE_UPDATE |
++ FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK, ptr) |
++ (data & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK);
++
++ return mv88e6xxx_port_write(chip, port,
++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, reg);
++}
++
++static int mv88e6390_port_ippmt_read(struct mv88e6xxx_chip *chip, int port,
++ u8 ptr, u16 *data)
++{
++ u16 reg;
++ int err;
++
++ reg = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK, ptr);
++
++ err = mv88e6xxx_port_write(chip, port,
++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, reg);
++ if (err)
++ return err;
++
++ err = mv88e6xxx_port_read(chip, port,
++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, ®);
++ if (err)
++ return err;
++
++ *data = reg & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK;
++
++ return 0;
++}
++
++int mv88e6390_port_get_dscp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp)
++{
++ u16 data;
++ int err;
++
++ err = mv88e6390_port_ippmt_read(chip, port, dscp, &data);
++ if (err)
++ return err;
++
++ if (data & (MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI |
++ MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI))
++ return -EOPNOTSUPP;
++
++ return FIELD_GET(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, data);
++}
++
++int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp, int prio)
++{
++ u16 data;
++
++ if (prio < 0)
++ data = MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI |
++ MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI;
++ else
++ data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK, prio) |
++ FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, prio);
++
++ return mv88e6390_port_ippmt_write(chip, port, dscp, data);
++}
++
+ /* Offset 0x18: Port IEEE Priority Remapping Registers [0-3]
+ * Offset 0x19: Port IEEE Priority Remapping Registers [4-7]
+ */
+@@ -1711,6 +1908,74 @@ int mv88e6390_port_tag_remap(struct mv88e6xxx_chip *chip, int port)
+ return 0;
+ }
+
++static int mv88e6xxx_port_ieeepmt_read(struct mv88e6xxx_chip *chip,
++ int port, u16 table, u8 ptr, u16 *data)
++{
++ u16 reg;
++ int err;
++
++ reg = table |
++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PTR_MASK, ptr);
++
++ err = mv88e6xxx_port_write(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE, reg);
++ if (err)
++ return err;
++
++ err = mv88e6xxx_port_read(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE, ®);
++ if (err)
++ return err;
++
++ *data = reg & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DATA_MASK;
++
++ return 0;
++}
++
++static u16 mv88e6390_port_ingress_pcp_table(u8 dei)
++{
++ if (dei)
++ return MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP_DEI;
++
++ return MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP;
++}
++
++int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei)
++{
++ u16 data;
++ int err;
++
++ err = mv88e6xxx_port_ieeepmt_read(chip, port,
++ mv88e6390_port_ingress_pcp_table(dei),
++ pcp, &data);
++ if (err)
++ return err;
++
++ if (data & (MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI |
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI))
++ return -EOPNOTSUPP;
++
++ return FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, data);
++}
++
++int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei, int prio)
++{
++ u16 data;
++
++ if (prio < 0)
++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI |
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI;
++ else
++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK, prio) |
++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, prio);
++
++ return mv88e6xxx_port_ieeepmt_write(chip, port,
++ mv88e6390_port_ingress_pcp_table(dei),
++ pcp, data);
++}
++
+ /* Offset 0x0E: Policy Control Register */
+
+ static int
+diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
+index 5f733f84b348..a9f2ed6ca79c 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.h
++++ b/drivers/net/dsa/mv88e6xxx/port.h
+@@ -211,6 +211,7 @@
+
+ /* Offset 0x07: Default Port VLAN ID & Priority */
+ #define MV88E6XXX_PORT_DEFAULT_VLAN 0x07
++#define MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK 0xe000
+ #define MV88E6XXX_PORT_DEFAULT_VLAN_MASK 0x0fff
+
+ /* Offset 0x08: Port Control 2 Register */
+@@ -235,6 +236,7 @@
+ #define MV88E6XXX_PORT_CTL2_EGRESS_MONITOR 0x0020
+ #define MV88E6XXX_PORT_CTL2_INGRESS_MONITOR 0x0010
+ #define MV88E6095_PORT_CTL2_CPU_PORT_MASK 0x000f
++#define MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK 0x0007
+
+ /* Offset 0x09: Egress Rate Control */
+ #define MV88E6XXX_PORT_EGRESS_RATE_CTL1 0x09
+@@ -449,6 +451,17 @@
+ /* Offset 0x16: LED Control (6393X family) */
+ #define MV88E6393X_PORT_LED_CONTROL 0x16
+
++/* Offset 0x17: IP Priority Mapping Table */
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE 0x17
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_UPDATE 0x8000
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK 0x7e00
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK 0x01ff
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_YELLOW 0x0100
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI 0x0080
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK 0x0070
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI 0x0008
++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK 0x0007
++
+ /* Offset 0x18: IEEE Priority Mapping Table */
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE 0x18
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_UPDATE 0x8000
+@@ -457,11 +470,25 @@
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP 0x1000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_PCP 0x2000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_AVB_PCP 0x3000
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP_DEI 0x4000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP 0x5000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_DSCP 0x6000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_AVB_DSCP 0x7000
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PTR_MASK 0x0e00
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DATA_MASK 0x01ff
++/* Ingress PCP tables */
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_YELLOW 0x0100
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI 0x0080
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK 0x0070
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI 0x0008
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK 0x0007
++/* Egress PCP tables */
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_VID 0x0010
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP 0x0008
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK 0x0007
++/* Egress DSCP tables */
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP 0x0040
++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK 0x003f
+
+ /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */
+ #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18
+@@ -543,6 +570,21 @@ int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port,
+ u16 mode);
+ int mv88e6095_port_tag_remap(struct mv88e6xxx_chip *chip, int port);
+ int mv88e6390_port_tag_remap(struct mv88e6xxx_chip *chip, int port);
++int mv88e6xxx_port_set_apptrust(struct mv88e6xxx_chip *chip, int port,
++ const u8 *sel, int nsel);
++int mv88e6xxx_port_get_apptrust(struct mv88e6xxx_chip *chip, int port,
++ u8 *sel, int *nsel);
++int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port);
++int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 prio);
++int mv88e6390_port_get_dscp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp);
++int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 dscp, int prio);
++int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei);
++int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
++ u8 pcp, u8 dei, int prio);
+ int mv88e6xxx_port_set_egress_mode(struct mv88e6xxx_chip *chip, int port,
+ enum mv88e6xxx_egress_mode mode);
+ int mv88e6085_port_set_frame_mode(struct mv88e6xxx_chip *chip, int port,
diff --git a/patches/linux/6.18.59/0135-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch b/patches/linux/6.18.59/0135-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch
new file mode 100644
index 0000000000..47ef493cc6
--- /dev/null
+++ b/patches/linux/6.18.59/0135-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch
@@ -0,0 +1,430 @@
+From dffcc4845099da3a5ad1c7b33b1980abd73ac388 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:12:40 +0200
+Subject: [PATCH 135/139] net: dsa: mv88e6xxx: Add DCB priority rewrite
+Organization: Wires
+
+On egress the 6390 family looks up the frame priority assigned at
+ingress in the IEEE Priority Mapping Table to rewrite the PCP of tagged
+frames and, when enabled, the DSCP of IP frames. The driver programs
+an identity PCP rewrite at setup and leaves DSCP rewrite off, with no
+way to change either.
+
+Hook the tables up to the DSA DCB rewrite ops:
+
+ dcb rewr add dev lan1 prio-pcp 5:5nd prio-dscp 5:40
+ dcb rewr del dev lan1 prio-pcp 5:5nd
+
+Deleting a PCP entry sets DisPCP for that priority, so tagged frames
+keep their PCP. DSCP marking is also gated per port by Update DSCP in
+Port Control 3, which is set while any priority has an entry. The
+green and yellow tables are kept equal since the color is not used, and
+the AVB tables are left as they are. The DEI of a remarked frame
+follows its color and cannot be set from the table, so only DEI 0
+entries are accepted.
+
+Signed-off-by: Joachim Wiberg
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 140 +++++++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/chip.h | 12 +++
+ drivers/net/dsa/mv88e6xxx/port.c | 125 +++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/port.h | 13 +++
+ 4 files changed, 290 insertions(+)
+
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
+index 139c3cda1079..30fb3c587cd8 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4861,6 +4861,10 @@ static const struct mv88e6xxx_ops mv88e6190_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -4927,6 +4931,10 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -4993,6 +5001,10 @@ static const struct mv88e6xxx_ops mv88e6191_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+ .port_set_mcast_flood = mv88e6352_port_set_mcast_flood,
+@@ -5163,6 +5175,10 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5560,6 +5576,10 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5630,6 +5650,10 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6352_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -5699,6 +5723,10 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr,
+ .port_set_policy = mv88e6393x_port_set_policy,
+ .port_set_frame_mode = mv88e6351_port_set_frame_mode,
+ .port_set_ucast_flood = mv88e6352_port_set_ucast_flood,
+@@ -7347,6 +7375,112 @@ static int mv88e6xxx_port_del_pcp_prio(struct dsa_switch *ds, int port,
+ return err;
+ }
+
++static int mv88e6xxx_port_get_pcp_rewr(struct dsa_switch *ds, int port,
++ u8 prio, u8 *pcp, u8 *dei)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_get_pcp_rewr)
++ return -EOPNOTSUPP;
++
++ *dei = 0;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_pcp_rewr(chip, port, prio, pcp);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++/* The DEI of a remarked frame follows its color, it cannot be set from
++ * the table, so only the DEI 0 entries can be honored.
++ */
++static int mv88e6xxx_port_set_pcp_rewr(struct dsa_switch *ds, int port,
++ u8 prio, u8 pcp, u8 dei)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_pcp_rewr)
++ return -EOPNOTSUPP;
++
++ if (dei) {
++ dev_err(chip->dev, "p%d: PCP rewrite cannot set DEI\n", port);
++ return -EINVAL;
++ }
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_pcp_rewr(chip, port, prio, pcp);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_del_pcp_rewr(struct dsa_switch *ds, int port,
++ u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_pcp_rewr)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_pcp_rewr(chip, port, prio, -1);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_get_dscp_rewr(struct dsa_switch *ds, int port,
++ u8 prio, u8 *dscp)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_get_dscp_rewr)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_get_dscp_rewr(chip, port, prio, dscp);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_set_dscp_rewr(struct dsa_switch *ds, int port,
++ u8 prio, u8 dscp)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_dscp_rewr)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_dscp_rewr(chip, port, prio, dscp);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
++static int mv88e6xxx_port_del_dscp_rewr(struct dsa_switch *ds, int port,
++ u8 prio)
++{
++ struct mv88e6xxx_chip *chip = ds->priv;
++ int err;
++
++ if (!chip->info->ops->port_set_dscp_rewr)
++ return -EOPNOTSUPP;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_dscp_rewr(chip, port, prio, -1);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
+ static int mv88e6xxx_dcb_set_apptrust(struct dsa_switch *ds, int port,
+ const u8 *sel, int nsel)
+ {
+@@ -7569,6 +7703,12 @@ static const struct dsa_switch_ops mv88e6xxx_switch_ops = {
+ .port_del_pcp_prio = mv88e6xxx_port_del_pcp_prio,
+ .port_set_apptrust = mv88e6xxx_dcb_set_apptrust,
+ .port_get_apptrust = mv88e6xxx_dcb_get_apptrust,
++ .port_get_pcp_rewr = mv88e6xxx_port_get_pcp_rewr,
++ .port_set_pcp_rewr = mv88e6xxx_port_set_pcp_rewr,
++ .port_del_pcp_rewr = mv88e6xxx_port_del_pcp_rewr,
++ .port_get_dscp_rewr = mv88e6xxx_port_get_dscp_rewr,
++ .port_set_dscp_rewr = mv88e6xxx_port_set_dscp_rewr,
++ .port_del_dscp_rewr = mv88e6xxx_port_del_dscp_rewr,
+ .port_add_etype_prio = mv88e6xxx_port_add_etype_prio,
+ .port_del_etype_prio = mv88e6xxx_port_del_etype_prio,
+ .port_setup_tc = mv88e6xxx_port_setup_tc,
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
+index bb5739cf8cf9..c0af84346b89 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.h
++++ b/drivers/net/dsa/mv88e6xxx/chip.h
+@@ -591,6 +591,18 @@ struct mv88e6xxx_ops {
+ int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
+
++ /* Egress remarking from the frame priority, a negative code
++ * point disables it for that priority.
++ */
++ int (*port_get_pcp_rewr)(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *pcp);
++ int (*port_set_pcp_rewr)(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int pcp);
++ int (*port_get_dscp_rewr)(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *dscp);
++ int (*port_set_dscp_rewr)(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int dscp);
++
+ int (*port_set_policy)(struct mv88e6xxx_chip *chip, int port,
+ enum mv88e6xxx_policy_mapping mapping,
+ enum mv88e6xxx_policy_action action);
+diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
+index f7991eeb9d17..92618049e889 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.c
++++ b/drivers/net/dsa/mv88e6xxx/port.c
+@@ -1976,6 +1976,131 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ pcp, data);
+ }
+
++/* Egress remarking uses the frame priority assigned at ingress to look
++ * up the PCP written to tagged frames and the DSCP written to IP frames.
++ * Green and yellow frames have separate tables, both are kept equal as
++ * the driver does not use the color. The AVB tables are left alone.
++ */
++int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *pcp)
++{
++ u16 data;
++ int err;
++
++ err = mv88e6xxx_port_ieeepmt_read(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP,
++ prio, &data);
++ if (err)
++ return err;
++
++ if (data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP)
++ return -ENOENT;
++
++ *pcp = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK, data);
++
++ return 0;
++}
++
++int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int pcp)
++{
++ u16 data;
++ int err;
++
++ if (pcp < 0)
++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP;
++ else
++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK, pcp);
++
++ err = mv88e6xxx_port_ieeepmt_write(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP,
++ prio, data);
++ if (err)
++ return err;
++
++ return mv88e6xxx_port_ieeepmt_write(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_PCP,
++ prio, data);
++}
++
++/* Port Control 3 gates DSCP marking for the whole port, keep it set as
++ * long as any priority has an enabled entry.
++ */
++static int mv88e6390_port_update_dscp(struct mv88e6xxx_chip *chip, int port)
++{
++ u16 table = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP;
++ bool enable = false;
++ u16 data, reg;
++ int err, prio;
++
++ for (prio = 0; prio < 8 && !enable; prio++) {
++ err = mv88e6xxx_port_ieeepmt_read(chip, port, table, prio,
++ &data);
++ if (err)
++ return err;
++
++ enable = data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP;
++ }
++
++ err = mv88e6xxx_port_read(chip, port, MV88E6390_PORT_CTL3, ®);
++ if (err)
++ return err;
++
++ if (enable)
++ reg |= MV88E6390_PORT_CTL3_UPDATE_DSCP;
++ else
++ reg &= ~MV88E6390_PORT_CTL3_UPDATE_DSCP;
++
++ return mv88e6xxx_port_write(chip, port, MV88E6390_PORT_CTL3, reg);
++}
++
++int mv88e6390_port_get_dscp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *dscp)
++{
++ u16 data;
++ int err;
++
++ err = mv88e6xxx_port_ieeepmt_read(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP,
++ prio, &data);
++ if (err)
++ return err;
++
++ if (!(data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP))
++ return -ENOENT;
++
++ *dscp = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK, data);
++
++ return 0;
++}
++
++int mv88e6390_port_set_dscp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int dscp)
++{
++ u16 data;
++ int err;
++
++ if (dscp < 0)
++ data = 0;
++ else
++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP |
++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK, dscp);
++
++ err = mv88e6xxx_port_ieeepmt_write(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP,
++ prio, data);
++ if (err)
++ return err;
++
++ err = mv88e6xxx_port_ieeepmt_write(chip, port,
++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_DSCP,
++ prio, data);
++ if (err)
++ return err;
++
++ return mv88e6390_port_update_dscp(chip, port);
++}
++
+ /* Offset 0x0E: Policy Control Register */
+
+ static int
+diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
+index a9f2ed6ca79c..ef5028707ec5 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.h
++++ b/drivers/net/dsa/mv88e6xxx/port.h
+@@ -490,6 +490,11 @@
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP 0x0040
+ #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK 0x003f
+
++/* Offset 0x19: Port Control 3 */
++#define MV88E6390_PORT_CTL3 0x19
++#define MV88E6390_PORT_CTL3_UPDATE_DSCP 0x0020
++#define MV88E6390_PORT_CTL3_UPDATE_DEI 0x0008
++
+ /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */
+ #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18
+
+@@ -585,6 +590,14 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei);
+ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
++int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *pcp);
++int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int pcp);
++int mv88e6390_port_get_dscp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, u8 *dscp);
++int mv88e6390_port_set_dscp_rewr(struct mv88e6xxx_chip *chip, int port,
++ u8 prio, int dscp);
+ int mv88e6xxx_port_set_egress_mode(struct mv88e6xxx_chip *chip, int port,
+ enum mv88e6xxx_egress_mode mode);
+ int mv88e6085_port_set_frame_mode(struct mv88e6xxx_chip *chip, int port,
diff --git a/patches/linux/6.18.59/0136-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch b/patches/linux/6.18.59/0136-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch
new file mode 100644
index 0000000000..9eba23d679
--- /dev/null
+++ b/patches/linux/6.18.59/0136-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch
@@ -0,0 +1,395 @@
+From 40fba29ed7ede7ea3f26163cca997784f94a383c Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 18:31:30 +0200
+Subject: [PATCH 136/139] net: dsa: mv88e6xxx: Queue frames by the mqprio map
+Organization: Wires
+
+The mqprio offload only sets the netdev traffic class layout. The
+ingress tables assign the queue priority (QPri) equal to the frame
+priority, so switched frames always land in the queue of their
+priority number, whatever map 'tc qdisc add ... mqprio' installed.
+Frames sent by the CPU do follow the map, since the DSA tag carries
+the traffic class of the transmit queue.
+
+A frame's queue is chosen where it enters, by the ingress port's
+tables, not where it leaves. A map installed on one port can only
+take effect for switched frames if every port classifies by it, so
+the chip has one map: the most recent request defines it, and it is
+programmed into the PCP, DSCP and default priority entries of every
+user port. Each priority lands in the first queue of its traffic
+class. Ports that asked for a different map are not refused, since
+ports changing map one at a time would refuse each other, but they
+are no longer queued by their own map, which drivers of qdiscs that
+report offload can tell. The identity map is restored when no port
+asks for one.
+
+Signed-off-by: Joachim Wiberg
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 109 ++++++++++++++++++++++++++++++-
+ drivers/net/dsa/mv88e6xxx/chip.h | 14 ++++
+ drivers/net/dsa/mv88e6xxx/port.c | 66 +++++++++++++++++--
+ drivers/net/dsa/mv88e6xxx/port.h | 1 +
+ 4 files changed, 183 insertions(+), 7 deletions(-)
+
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
+index 30fb3c587cd8..20b1f3d74396 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -1694,6 +1694,9 @@ static int mv88e6xxx_pot_setup(struct mv88e6xxx_chip *chip)
+ for (i = 0; i < ARRAY_SIZE(chip->qpri_po); i++)
+ refcount_set(&chip->qpri_po[i].refcnt, 0);
+
++ for (i = 0; i < ARRAY_SIZE(chip->qpri); i++)
++ chip->qpri[i] = i;
++
+ if (chip->info->ops->pot_clear)
+ return chip->info->ops->pot_clear(chip);
+
+@@ -3406,7 +3409,7 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port)
+ struct fwnode_handle *port_fwnode;
+ struct dsa_switch *ds = chip->ds;
+ struct mv88e6xxx_port *p;
+- int err;
++ int err, i;
+ u16 reg;
+ u32 val;
+
+@@ -3608,6 +3611,10 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port)
+ return err;
+ }
+
++ for (i = 0; i < ARRAY_SIZE(chip->ports[port].qpri); i++)
++ chip->ports[port].qpri[i] = i;
++ chip->ports[port].qmap = false;
++
+ if (chip->info->ops->port_tag_remap) {
+ err = chip->info->ops->port_tag_remap(chip, port);
+ if (err)
+@@ -4861,6 +4868,7 @@ static const struct mv88e6xxx_ops mv88e6190_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -4931,6 +4939,7 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5001,6 +5010,7 @@ static const struct mv88e6xxx_ops mv88e6191_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5175,6 +5185,7 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5576,6 +5587,7 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5650,6 +5662,7 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5723,6 +5736,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
+ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio,
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
++ .port_sync_qpri = mv88e6390_port_sync_qpri,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -7562,12 +7576,96 @@ static int mv88e6xxx_port_del_etype_prio(struct dsa_switch *ds, int port,
+ return err;
+ }
+
++/* Program @qpri into the tables of every user port, if it is news */
++static int mv88e6xxx_sync_qpri(struct mv88e6xxx_chip *chip, const u8 *qpri)
++{
++ int port, err;
++
++ if (!memcmp(qpri, chip->qpri, sizeof(chip->qpri)))
++ return 0;
++
++ memcpy(chip->qpri, qpri, sizeof(chip->qpri));
++
++ if (!chip->info->ops->port_sync_qpri)
++ return 0;
++
++ for (port = 0; port < mv88e6xxx_num_ports(chip); port++) {
++ if (!dsa_is_user_port(chip->ds, port))
++ continue;
++
++ err = chip->info->ops->port_sync_qpri(chip, port);
++ if (err)
++ return err;
++ }
++
++ return 0;
++}
++
++/* Whether the chip queues by the map this port's qdisc asked for */
++static bool mv88e6xxx_port_qpri_in_use(struct mv88e6xxx_chip *chip, int port)
++{
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++
++ return mp->qmap && !memcmp(mp->qpri, chip->qpri, sizeof(mp->qpri));
++}
++
++/* Take the queue map a port's qdisc asks for, or drop it with NULL. The
++ * queue is chosen where a frame enters, by that port's tables, so the
++ * chip has one map: the most recent request defines it, and a port
++ * whose request differs is no longer offloaded, see
++ * mv88e6xxx_port_qpri_in_use(). The identity map is restored when no
++ * port asks for one.
++ */
++static int mv88e6xxx_port_set_qpri(struct mv88e6xxx_chip *chip, int port,
++ const u8 *qpri)
++{
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++ int other, prio, err;
++ u8 map[8];
++
++ if (qpri) {
++ for (prio = 0; prio < ARRAY_SIZE(map); prio++)
++ map[prio] = min_t(u8, qpri[prio], 7);
++
++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) {
++ if (other != port && mv88e6xxx_port_qpri_in_use(chip, other) &&
++ memcmp(map, chip->qpri, sizeof(map))) {
++ dev_info(chip->dev, "p%d: queue map replaces the one p%d asked for, the chip has one\n",
++ port, other);
++ break;
++ }
++ }
++
++ memcpy(mp->qpri, map, sizeof(mp->qpri));
++ mp->qmap = true;
++ } else {
++ mp->qmap = false;
++
++ for (prio = 0; prio < ARRAY_SIZE(map); prio++)
++ map[prio] = prio;
++
++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) {
++ if (chip->ports[other].qmap) {
++ memcpy(map, chip->qpri, sizeof(map));
++ break;
++ }
++ }
++ }
++
++ mv88e6xxx_reg_lock(chip);
++ err = mv88e6xxx_sync_qpri(chip, map);
++ mv88e6xxx_reg_unlock(chip);
++
++ return err;
++}
++
+ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port,
+ struct tc_mqprio_qopt_offload *mqprio)
+ {
+ struct net_device *dev = dsa_to_port(chip->ds, port)->user;
+ struct tc_mqprio_qopt *qopt = &mqprio->qopt;
+- int tc, err = 0, num_txq = 0;
++ int tc, prio, err = 0, num_txq = 0;
++ u8 qpri[8];
+
+ if (!qopt->num_tc)
+ goto out_reset;
+@@ -7588,10 +7686,15 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port,
+ if (err)
+ goto out_reset;
+
+- return 0;
++ /* The first queue of the priority's traffic class */
++ for (prio = 0; prio < ARRAY_SIZE(qpri); prio++)
++ qpri[prio] = qopt->offset[qopt->prio_tc_map[prio]];
++
++ return mv88e6xxx_port_set_qpri(chip, port, qpri);
+
+ out_reset:
+ netdev_reset_tc(dev);
++ mv88e6xxx_port_set_qpri(chip, port, NULL);
+ return err;
+
+ }
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
+index c0af84346b89..f0736aeebbb8 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.h
++++ b/drivers/net/dsa/mv88e6xxx/chip.h
+@@ -306,6 +306,12 @@ struct mv88e6xxx_port {
+ refcount_t refcnt;
+ u16 proto;
+ } etype;
++
++ /* Egress queue per frame priority this port's qdisc asked for,
++ * meaningful while qmap is set
++ */
++ bool qmap;
++ u8 qpri[8];
+ };
+
+ enum mv88e6xxx_region_id {
+@@ -465,6 +471,13 @@ struct mv88e6xxx_chip {
+
+ /* Queue priority overrides */
+ struct mv88e6xxx_po qpri_po[16];
++
++ /* Egress queue per frame priority. A frame's queue is chosen
++ * where it enters, by that port's tables, so the map is one per
++ * chip, programmed into every user port, and follows the port
++ * that asked most recently.
++ */
++ u8 qpri[8];
+ };
+
+ struct mv88e6xxx_bus_ops {
+@@ -590,6 +603,7 @@ struct mv88e6xxx_ops {
+ u8 pcp, u8 dei);
+ int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
++ int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port);
+
+ /* Egress remarking from the frame priority, a negative code
+ * point disables it for that priority.
+diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
+index 92618049e889..255f6229a13a 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.c
++++ b/drivers/net/dsa/mv88e6xxx/port.c
+@@ -1214,8 +1214,18 @@ int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid)
+ return 0;
+ }
+
++/* Frames are queued by their queue priority (QPri), which the ingress
++ * tables assign alongside the frame priority (FPri). The chip's queue
++ * map decides which queue a priority lands in; identity until one is
++ * set.
++ */
++static u8 mv88e6xxx_qpri(struct mv88e6xxx_chip *chip, u8 prio)
++{
++ return chip->qpri[prio & 0x7];
++}
++
+ /* The default frame priority (FPri) lives here, the default queue
+- * priority (QPri) in Port Control 2. Both are set to the same value.
++ * priority (QPri) in Port Control 2.
+ */
+ int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port)
+ {
+@@ -1254,7 +1264,8 @@ int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port,
+ return err;
+
+ reg &= ~MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK;
+- reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK, prio);
++ reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK,
++ mv88e6xxx_qpri(chip, prio));
+
+ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL2, reg);
+ }
+@@ -1838,7 +1849,8 @@ int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port,
+ data = MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI |
+ MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI;
+ else
+- data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK, prio) |
++ data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK,
++ mv88e6xxx_qpri(chip, prio)) |
+ FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, prio);
+
+ return mv88e6390_port_ippmt_write(chip, port, dscp, data);
+@@ -1968,7 +1980,8 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI |
+ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI;
+ else
+- data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK, prio) |
++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK,
++ mv88e6xxx_qpri(chip, prio)) |
+ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, prio);
+
+ return mv88e6xxx_port_ieeepmt_write(chip, port,
+@@ -1976,6 +1989,51 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ pcp, data);
+ }
+
++/* Rewrite the QPri of every enabled entry after the mqprio map changed */
++int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port)
++{
++ int err, i, prio;
++ u16 table, data;
++
++ for (i = 0; i < 64; i++) {
++ err = mv88e6390_port_ippmt_read(chip, port, i, &data);
++ if (err)
++ return err;
++
++ if (data & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI)
++ continue;
++
++ prio = FIELD_GET(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, data);
++ err = mv88e6390_port_set_dscp_prio(chip, port, i, prio);
++ if (err)
++ return err;
++ }
++
++ for (i = 0; i < 16; i++) {
++ table = mv88e6390_port_ingress_pcp_table(i >> 3);
++
++ err = mv88e6xxx_port_ieeepmt_read(chip, port, table, i & 0x7,
++ &data);
++ if (err)
++ return err;
++
++ if (data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI)
++ continue;
++
++ prio = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, data);
++ err = mv88e6390_port_set_pcp_prio(chip, port, i & 0x7, i >> 3,
++ prio);
++ if (err)
++ return err;
++ }
++
++ err = mv88e6390_port_get_default_prio(chip, port);
++ if (err < 0)
++ return err;
++
++ return mv88e6390_port_set_default_prio(chip, port, err);
++}
++
+ /* Egress remarking uses the frame priority assigned at ingress to look
+ * up the PCP written to tagged frames and the DSCP written to IP frames.
+ * Green and yellow frames have separate tables, both are kept equal as
+diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
+index ef5028707ec5..273dacfe2439 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.h
++++ b/drivers/net/dsa/mv88e6xxx/port.h
+@@ -590,6 +590,7 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei);
+ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
++int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port);
+ int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
+ u8 prio, u8 *pcp);
+ int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
diff --git a/patches/linux/6.18.59/0137-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch b/patches/linux/6.18.59/0137-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch
new file mode 100644
index 0000000000..ecbbd8d6f4
--- /dev/null
+++ b/patches/linux/6.18.59/0137-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch
@@ -0,0 +1,56 @@
+From adc072e25b2a1f1c723074a81f61352ed691bee1 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Fri, 11 Sep 2026 15:54:16 +0200
+Subject: [PATCH 137/139] net: dsa: tag_dsa: Read the tag priority before the
+ header is stripped
+Organization: Wires
+
+Routed frames left the switch with a priority unrelated to their
+DSCP, constant for a given sender. The tag priority is copied to
+skb->priority after the DSA header has been removed, and for untagged
+frames dsa_strip_etype_header() has by then moved the MAC addresses
+over the old header bytes, so the value read is a byte of the source
+MAC address. Tagged frames were unaffected since the 802.1Q tag
+written in place keeps the PCP bits where the DSA PRI field was.
+
+Read the priority right after the header is parsed.
+
+Signed-off-by: Joachim Wiberg
+---
+ net/dsa/tag_dsa.c | 16 +++++++++-------
+ 1 file changed, 9 insertions(+), 7 deletions(-)
+
+diff --git a/net/dsa/tag_dsa.c b/net/dsa/tag_dsa.c
+index 55c296e0b5b0..35595aa21f52 100644
+--- a/net/dsa/tag_dsa.c
++++ b/net/dsa/tag_dsa.c
+@@ -276,6 +276,15 @@ static struct sk_buff *dsa_rcv_ll(struct sk_buff *skb, struct net_device *dev,
+ if (!skb->dev)
+ return NULL;
+
++ /* Ideally we would have a way of configuring the mapping of PCP
++ * bits to skb->priority on DSA ports (like ingress-qos-map for
++ * vlan interfaces). For now, assume that a 1:1 mapping is more
++ * useful than ignoring the priority altogether. Read it before
++ * the header is stripped below, which moves the MAC addresses
++ * over these bytes.
++ */
++ skb->priority = dsa_header[2] >> 5;
++
+ /* When using LAG offload, skb->dev is not a DSA user interface,
+ * so we cannot call dsa_default_offload_fwd_mark and we need to
+ * special-case it.
+@@ -325,13 +334,6 @@ static struct sk_buff *dsa_rcv_ll(struct sk_buff *skb, struct net_device *dev,
+ dsa_strip_etype_header(skb, DSA_HLEN + extra);
+ }
+
+- /* Ideally we would have a way of configuring the mapping of PCP
+- * bits to skb->priority on DSA ports (like ingress-qos-map for
+- * vlan interfaces). For now, assume that a 1:1 mapping is more
+- * useful than ignoring the priority altogether.
+- */
+- skb->priority = dsa_header[2] >> 5;
+-
+ return skb;
+ }
+
diff --git a/patches/linux/6.18.59/0138-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch b/patches/linux/6.18.59/0138-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch
new file mode 100644
index 0000000000..a538b2606a
--- /dev/null
+++ b/patches/linux/6.18.59/0138-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch
@@ -0,0 +1,272 @@
+From c7476a26e6062d9b6767793bc54b48dc1f2b2008 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 17 Sep 2026 17:09:02 +0200
+Subject: [PATCH 138/139] net: dsa: mv88e6xxx: Offload the tbf qdisc
+Organization: Wires
+
+A tbf qdisc on a switch port shapes only what the CPU sends; frames
+switched between ports never pass the qdisc. The chip has an egress
+rate limiter per port that does the same job for both.
+
+Take a root tbf and program its rate into the port's Egress Rate
+Control, counting Layer 2 bytes as the qdisc does. The limiter steps
+in fixed increments per decade of rates, 64 kbit/s below 1 Mbit/s up
+to 100 Mbit/s below 10 Gbit/s, so the rate is rounded to the nearest
+step. A burst size has no hardware equivalent and is ignored. A
+child qdisc is accepted and left to bring its own offload.
+
+Signed-off-by: Joachim Wiberg
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 52 ++++++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/chip.h | 7 +++++
+ drivers/net/dsa/mv88e6xxx/port.c | 54 ++++++++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/port.h | 9 ++++++
+ 4 files changed, 122 insertions(+)
+
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
+index 20b1f3d74396..c1963a94e849 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -32,6 +32,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+
+ #include "chip.h"
+@@ -4869,6 +4870,7 @@ static const struct mv88e6xxx_ops mv88e6190_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -4940,6 +4942,7 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5011,6 +5014,7 @@ static const struct mv88e6xxx_ops mv88e6191_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5186,6 +5190,7 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5588,6 +5593,7 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5663,6 +5669,7 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -5737,6 +5744,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr,
+@@ -7696,7 +7704,49 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port,
+ netdev_reset_tc(dev);
+ mv88e6xxx_port_set_qpri(chip, port, NULL);
+ return err;
++}
++
++static int mv88e6xxx_qos_port_tbf(struct mv88e6xxx_chip *chip, int port,
++ struct tc_tbf_qopt_offload *qopt)
++{
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++ u64 bps;
++ int err;
++
++ if (!chip->info->ops->port_set_egress_rate)
++ return -EOPNOTSUPP;
++
++ if (qopt->parent != TC_H_ROOT)
++ return -EOPNOTSUPP;
++
++ switch (qopt->command) {
++ case TC_TBF_REPLACE:
++ bps = qopt->replace_params.rate.rate_bytes_ps * 8;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_egress_rate(chip, port, bps);
++ mv88e6xxx_reg_unlock(chip);
++ if (err)
++ return err;
+
++ mp->tbf_handle = qopt->handle;
++ return 0;
++ case TC_TBF_DESTROY:
++ mp->tbf_handle = 0;
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_egress_rate(chip, port, 0);
++ mv88e6xxx_reg_unlock(chip);
++ return err;
++ case TC_TBF_STATS:
++ case TC_TBF_GRAFT:
++ /* The child brings its own offload, and stats stay in
++ * software; either is what marks the tbf offloaded
++ */
++ return mp->tbf_handle ? 0 : -EOPNOTSUPP;
++ default:
++ return -EOPNOTSUPP;
++ }
+ }
+
+ static int mv88e6xxx_qos_query_caps(struct tc_query_caps_base *base)
+@@ -7722,6 +7772,8 @@ static int mv88e6xxx_port_setup_tc(struct dsa_switch *ds, int port,
+ return mv88e6xxx_qos_query_caps(type_data);
+ case TC_SETUP_QDISC_MQPRIO:
+ return mv88e6xxx_qos_port_mqprio(chip, port, type_data);
++ case TC_SETUP_QDISC_TBF:
++ return mv88e6xxx_qos_port_tbf(chip, port, type_data);
+ default:
+ return -EOPNOTSUPP;
+ }
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
+index f0736aeebbb8..b44331700887 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.h
++++ b/drivers/net/dsa/mv88e6xxx/chip.h
+@@ -312,6 +312,9 @@ struct mv88e6xxx_port {
+ */
+ bool qmap;
+ u8 qpri[8];
++
++ /* Offloaded tbf qdisc at the root, 0 when none */
++ u32 tbf_handle;
+ };
+
+ enum mv88e6xxx_region_id {
+@@ -605,6 +608,10 @@ struct mv88e6xxx_ops {
+ u8 pcp, u8 dei, int prio);
+ int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port);
+
++ /* Egress shaper in bits per second, 0 turns it off */
++ int (*port_set_egress_rate)(struct mv88e6xxx_chip *chip, int port,
++ u64 bps);
++
+ /* Egress remarking from the frame priority, a negative code
+ * point disables it for that priority.
+ */
+diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
+index 255f6229a13a..47834060cabf 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.c
++++ b/drivers/net/dsa/mv88e6xxx/port.c
+@@ -1480,6 +1480,60 @@ int mv88e6097_port_egress_rate_limiting(struct mv88e6xxx_chip *chip, int port)
+ 0x0001);
+ }
+
++/* Shape the port to a Layer 2 bit rate, or turn the shaper off with 0.
++ * The rate is a decrement value stepping a counter at one of four fixed
++ * intervals, each covering a decade of rates, so it is rounded to the
++ * nearest step of that decade.
++ */
++int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port,
++ u64 bps)
++{
++ u32 step, rate;
++ u16 dec;
++ int err;
++
++ if (!bps) {
++ err = mv88e6xxx_port_write(chip, port,
++ MV88E6XXX_PORT_EGRESS_RATE_CTL2, 0);
++ if (err)
++ return err;
++
++ return mv88e6097_port_egress_rate_limiting(chip, port);
++ }
++
++ if (bps < 64000) {
++ return -ERANGE;
++ } else if (bps < 1000000) {
++ step = 64000;
++ rate = 0x1e84;
++ } else if (bps < 100000000) {
++ step = 1000000;
++ rate = 0x01f4;
++ } else if (bps < 1000000000) {
++ step = 10000000;
++ rate = 0x0032;
++ } else if (bps <= 10000000000ULL) {
++ step = 100000000;
++ rate = 0x0005;
++ } else {
++ return -ERANGE;
++ }
++
++ dec = DIV_ROUND_CLOSEST_ULL(bps, step);
++ if (!dec || dec > MV88E6390_PORT_EGRESS_RATE_CTL1_DEC_MASK)
++ return -ERANGE;
++
++ err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_EGRESS_RATE_CTL1,
++ dec);
++ if (err)
++ return err;
++
++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_EGRESS_RATE_CTL2,
++ FIELD_PREP(MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_MASK,
++ MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER2) |
++ rate);
++}
++
+ /* Offset 0x0B: Port Association Vector */
+
+ int mv88e6xxx_port_set_assoc_vector(struct mv88e6xxx_chip *chip, int port,
+diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
+index 273dacfe2439..49674351b532 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.h
++++ b/drivers/net/dsa/mv88e6xxx/port.h
+@@ -240,9 +240,16 @@
+
+ /* Offset 0x09: Egress Rate Control */
+ #define MV88E6XXX_PORT_EGRESS_RATE_CTL1 0x09
++#define MV88E6390_PORT_EGRESS_RATE_CTL1_DEC_MASK 0x007f
+
+ /* Offset 0x0A: Egress Rate Control 2 */
+ #define MV88E6XXX_PORT_EGRESS_RATE_CTL2 0x0a
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_MASK 0xc000
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_FRAMES 0x0
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER1 0x1
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER2 0x2
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER3 0x3
++#define MV88E6390_PORT_EGRESS_RATE_CTL2_RATE_MASK 0x3fff
+
+ /* Offset 0x0B: Port Association Vector */
+ #define MV88E6XXX_PORT_ASSOC_VECTOR 0x0b
+@@ -591,6 +598,8 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
+ int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port);
++int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port,
++ u64 bps);
+ int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
+ u8 prio, u8 *pcp);
+ int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
diff --git a/patches/linux/6.18.59/0139-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch b/patches/linux/6.18.59/0139-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch
new file mode 100644
index 0000000000..1a393aa0e2
--- /dev/null
+++ b/patches/linux/6.18.59/0139-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch
@@ -0,0 +1,551 @@
+From 4dce4de3daeec5b49d7b733991297657ae420f22 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 17 Sep 2026 17:09:45 +0200
+Subject: [PATCH 139/139] net: dsa: mv88e6xxx: Offload the ets qdisc
+Organization: Wires
+
+An ets qdisc on a switch port schedules only what the CPU sends. The
+mqprio offload puts switched frames in the queue of their traffic
+class, but says nothing about how the queues are served, so the chip
+keeps its power-on weighted round robin whatever algorithm the user
+configured.
+
+Take an ets qdisc at the root, or under an offloaded tbf, and program
+the port from it. The strict bands become the strict queues in the
+port's schedule mode, the priority map becomes the chip's queue map,
+as for mqprio, and the weighted bands become the round robin weights.
+The chip walks one queue service sequence for all its ports, so the
+weights are shared like the queue map: the most recent request sets
+the weight of the queues it serves by weight, and a port that asked
+for another weight on one of them is no longer reported offloaded.
+The sequence is rebuilt from the chip's weights, interleaved evenly
+with each queue present at least once, and reverts to the power-on
+weights when no port is left. The sequence counts frames where ets
+quanta count bytes, so the shares hold for frames of similar size.
+
+Frames from the CPU are queued by the traffic class of their transmit
+queue, so lay the queues out one per class and map the priorities as
+the ingress tables do. Removing the qdisc drops the port's requests,
+the round robin schedule and the queue layout.
+
+Signed-off-by: Joachim Wiberg
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 256 ++++++++++++++++++++++++++++
+ drivers/net/dsa/mv88e6xxx/chip.h | 20 +++
+ drivers/net/dsa/mv88e6xxx/global2.c | 42 +++++
+ drivers/net/dsa/mv88e6xxx/global2.h | 5 +
+ drivers/net/dsa/mv88e6xxx/port.c | 28 +++
+ drivers/net/dsa/mv88e6xxx/port.h | 10 ++
+ 6 files changed, 361 insertions(+)
+
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c
+index c1963a94e849..bdc5be69c94a 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4870,6 +4870,8 @@ static const struct mv88e6xxx_ops mv88e6190_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -4942,6 +4944,8 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -5014,6 +5018,8 @@ static const struct mv88e6xxx_ops mv88e6191_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -5190,6 +5196,8 @@ static const struct mv88e6xxx_ops mv88e6290_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -5593,6 +5601,8 @@ static const struct mv88e6xxx_ops mv88e6390_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -5669,6 +5679,8 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -5744,6 +5756,8 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = {
+ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio,
+ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio,
+ .port_sync_qpri = mv88e6390_port_sync_qpri,
++ .port_set_sched = mv88e6390_port_set_sched,
++ .set_qos_weights = mv88e6390_g2_set_qos_weights,
+ .port_set_egress_rate = mv88e6390_port_set_egress_rate,
+ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr,
+ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr,
+@@ -7706,6 +7720,246 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port,
+ return err;
+ }
+
++/* The DSA tag carries the traffic class of the transmit queue, so lay the
++ * queues out one per class and map the priorities like the ingress tables
++ * do, for frames from the CPU to land in the same queue as switched ones.
++ */
++static int mv88e6xxx_port_set_tx_map(struct net_device *dev, const u8 *qpri)
++{
++ int tc, prio, err;
++
++ err = netdev_set_num_tc(dev, 8);
++ if (err)
++ return err;
++
++ for (tc = 0; tc < 8; tc++)
++ netdev_set_tc_queue(dev, tc, 1, tc);
++
++ for (prio = 0; prio < 8; prio++)
++ netdev_set_prio_tc_map(dev, prio, qpri[prio]);
++
++ return 0;
++}
++
++/* Band 0 is dequeued first, queue 7 is served first */
++static u8 mv88e6xxx_ets_queue(const struct tc_ets_qopt_offload_replace_params *p,
++ u8 band)
++{
++ return p->bands - 1 - min_t(u8, band, p->bands - 1);
++}
++
++/* Whether the chip's sequence serves this port's weighted queues by the
++ * weights its ets asked for; the chip walks one sequence for all ports
++ */
++static bool mv88e6xxx_port_wrr_in_use(struct mv88e6xxx_chip *chip, int port)
++{
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++ int q;
++
++ if (!mp->ets)
++ return false;
++
++ for (q = 0; q < 8; q++)
++ if (mp->wrr[q] && mp->wrr[q] != chip->wrr[q])
++ return false;
++
++ return true;
++}
++
++/* Interleave the queues evenly, each as many times as its weight, in a
++ * sequence that fits the table. Every queue appears at least once, or
++ * frames in it would never leave the port.
++ */
++static unsigned int mv88e6xxx_wrr_sequence(const u8 *weight, u8 *seq,
++ unsigned int max)
++{
++ unsigned int total, scale = 1, len, q, best;
++ int credit[8];
++ u8 w[8];
++
++ do {
++ total = 0;
++ for (q = 0; q < 8; q++) {
++ w[q] = max_t(u8, DIV_ROUND_CLOSEST(weight[q], scale), 1);
++ total += w[q];
++ }
++ scale++;
++ } while (total > max);
++
++ memset(credit, 0, sizeof(credit));
++ for (len = 0; len < total; len++) {
++ best = 0;
++ for (q = 0; q < 8; q++) {
++ credit[q] += w[q];
++ if (credit[q] > credit[best])
++ best = q;
++ }
++ credit[best] -= total;
++ seq[len] = best;
++ }
++
++ return len;
++}
++
++/* Load the chip's weights, or its power-on weights when no port asks
++ * for any
++ */
++static int mv88e6xxx_ets_load_weights(struct mv88e6xxx_chip *chip)
++{
++ static const u8 defaults[8] = { 1, 2, 3, 6, 12, 17, 25, 33 };
++ u8 seq[MV88E6390_G2_QOS_WEIGHTS_MAX_LEN];
++ const u8 *weight = chip->wrr;
++ int q;
++
++ for (q = 0; q < 8 && !chip->wrr[q]; q++)
++ ;
++ if (q == 8)
++ weight = defaults;
++
++ return chip->info->ops->set_qos_weights(chip, seq,
++ mv88e6xxx_wrr_sequence(weight, seq,
++ ARRAY_SIZE(seq)));
++}
++
++/* Forget the port's requests; the chip keeps its map and weights as
++ * long as any port asks for them, else the identity map and the
++ * power-on weights come back
++ */
++static int mv88e6xxx_ets_destroy(struct mv88e6xxx_chip *chip, int port)
++{
++ struct net_device *dev = dsa_to_port(chip->ds, port)->user;
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++ int other, q, err;
++
++ mp->ets = false;
++ memset(mp->wrr, 0, sizeof(mp->wrr));
++
++ memset(chip->wrr, 0, sizeof(chip->wrr));
++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++)
++ for (q = 0; q < 8; q++)
++ if (chip->ports[other].wrr[q])
++ chip->wrr[q] = chip->ports[other].wrr[q];
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_sched(chip, port, 0);
++ if (!err)
++ err = mv88e6xxx_ets_load_weights(chip);
++ mv88e6xxx_reg_unlock(chip);
++ if (err)
++ return err;
++
++ netdev_reset_tc(dev);
++
++ return mv88e6xxx_port_set_qpri(chip, port, NULL);
++}
++
++static int mv88e6xxx_ets_replace(struct mv88e6xxx_chip *chip, int port,
++ struct tc_ets_qopt_offload *qopt)
++{
++ struct tc_ets_qopt_offload_replace_params *p = &qopt->replace_params;
++ struct net_device *dev = dsa_to_port(chip->ds, port)->user;
++ struct mv88e6xxx_port *mp = &chip->ports[port];
++ unsigned int nstrict = 0, band;
++ u8 qpri[8], wrr[8] = {};
++ int prio, other, q, err;
++
++ if (qopt->parent != TC_H_ROOT &&
++ !(mp->tbf_handle && TC_H_MAJ(qopt->parent) == mp->tbf_handle)) {
++ dev_err(chip->dev, "p%d: ets must be the root or under an offloaded tbf\n",
++ port);
++ return -EOPNOTSUPP;
++ }
++
++ if (!p->bands || p->bands > 8)
++ return -EOPNOTSUPP;
++
++ /* Strict bands come first, with no quantum */
++ while (nstrict < p->bands && !p->quanta[nstrict])
++ nstrict++;
++
++ for (band = nstrict; band < p->bands; band++) {
++ if (!p->quanta[band])
++ return -EOPNOTSUPP;
++
++ wrr[mv88e6xxx_ets_queue(p, band)] = max_t(u8, p->weights[band], 1);
++ }
++
++ /* One sequence per chip: the most recent request defines the
++ * weight of the queues it serves by weight, a port that asked
++ * for another weight on one of them is no longer offloaded
++ */
++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) {
++ if (other == port || !mv88e6xxx_port_wrr_in_use(chip, other))
++ continue;
++
++ for (q = 0; q < 8; q++) {
++ if (!wrr[q] || !chip->ports[other].wrr[q] ||
++ wrr[q] == chip->ports[other].wrr[q])
++ continue;
++
++ dev_info(chip->dev, "p%d: weight of queue %d replaces the one p%d asked for, the chip has one set\n",
++ port, q, other);
++ break;
++ }
++ }
++
++ for (prio = 0; prio < ARRAY_SIZE(qpri); prio++)
++ qpri[prio] = mv88e6xxx_ets_queue(p, p->priomap[prio]);
++
++ mv88e6xxx_reg_lock(chip);
++ err = chip->info->ops->port_set_sched(chip, port,
++ 8 - (p->bands - nstrict));
++ if (err)
++ goto out_unlock;
++
++ memcpy(mp->wrr, wrr, sizeof(mp->wrr));
++ mp->ets = true;
++ for (q = 0; q < 8; q++)
++ if (wrr[q])
++ chip->wrr[q] = wrr[q];
++ err = mv88e6xxx_ets_load_weights(chip);
++out_unlock:
++ mv88e6xxx_reg_unlock(chip);
++ if (err)
++ goto out_reset;
++
++ err = mv88e6xxx_port_set_qpri(chip, port, qpri);
++ if (err)
++ goto out_reset;
++
++ err = mv88e6xxx_port_set_tx_map(dev, qpri);
++ if (err)
++ goto out_reset;
++
++ return 0;
++
++out_reset:
++ mv88e6xxx_ets_destroy(chip, port);
++ return err;
++}
++
++static int mv88e6xxx_qos_port_ets(struct mv88e6xxx_chip *chip, int port,
++ struct tc_ets_qopt_offload *qopt)
++{
++ if (!chip->info->ops->port_set_sched ||
++ !chip->info->ops->set_qos_weights)
++ return -EOPNOTSUPP;
++
++ switch (qopt->command) {
++ case TC_ETS_REPLACE:
++ return mv88e6xxx_ets_replace(chip, port, qopt);
++ case TC_ETS_DESTROY:
++ return mv88e6xxx_ets_destroy(chip, port);
++ case TC_ETS_STATS:
++ /* Nothing to report, but this is what marks it offloaded */
++ return mv88e6xxx_port_qpri_in_use(chip, port) &&
++ mv88e6xxx_port_wrr_in_use(chip, port) ? 0 : -EOPNOTSUPP;
++ case TC_ETS_GRAFT:
++ default:
++ return -EOPNOTSUPP;
++ }
++}
++
+ static int mv88e6xxx_qos_port_tbf(struct mv88e6xxx_chip *chip, int port,
+ struct tc_tbf_qopt_offload *qopt)
+ {
+@@ -7772,6 +8026,8 @@ static int mv88e6xxx_port_setup_tc(struct dsa_switch *ds, int port,
+ return mv88e6xxx_qos_query_caps(type_data);
+ case TC_SETUP_QDISC_MQPRIO:
+ return mv88e6xxx_qos_port_mqprio(chip, port, type_data);
++ case TC_SETUP_QDISC_ETS:
++ return mv88e6xxx_qos_port_ets(chip, port, type_data);
+ case TC_SETUP_QDISC_TBF:
+ return mv88e6xxx_qos_port_tbf(chip, port, type_data);
+ default:
+diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h
+index b44331700887..7bf7cdea0bb1 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.h
++++ b/drivers/net/dsa/mv88e6xxx/chip.h
+@@ -313,6 +313,12 @@ struct mv88e6xxx_port {
+ bool qmap;
+ u8 qpri[8];
+
++ /* Offloaded ets qdisc, with the round robin weight per queue it
++ * asked for, 0 for queues it serves strictly or not at all
++ */
++ bool ets;
++ u8 wrr[8];
++
+ /* Offloaded tbf qdisc at the root, 0 when none */
+ u32 tbf_handle;
+ };
+@@ -481,6 +487,11 @@ struct mv88e6xxx_chip {
+ * that asked most recently.
+ */
+ u8 qpri[8];
++
++ /* Round robin weight per queue in the loaded service sequence,
++ * one sequence per chip like the queue map; 0 where no port asked
++ */
++ u8 wrr[8];
+ };
+
+ struct mv88e6xxx_bus_ops {
+@@ -608,6 +619,15 @@ struct mv88e6xxx_ops {
+ u8 pcp, u8 dei, int prio);
+ int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port);
+
++ /* Transmission selection: the number of highest queues served
++ * strictly, the rest by weighted round robin; the weights are a
++ * queue service sequence shared by all ports of the chip.
++ */
++ int (*port_set_sched)(struct mv88e6xxx_chip *chip, int port,
++ unsigned int strict);
++ int (*set_qos_weights)(struct mv88e6xxx_chip *chip, const u8 *seq,
++ unsigned int len);
++
+ /* Egress shaper in bits per second, 0 turns it off */
+ int (*port_set_egress_rate)(struct mv88e6xxx_chip *chip, int port,
+ u64 bps);
+diff --git a/drivers/net/dsa/mv88e6xxx/global2.c b/drivers/net/dsa/mv88e6xxx/global2.c
+index 3f16a7ef3fa2..a23541e3cb73 100644
+--- a/drivers/net/dsa/mv88e6xxx/global2.c
++++ b/drivers/net/dsa/mv88e6xxx/global2.c
+@@ -297,6 +297,48 @@ int mv88e6xxx_g2_set_switch_mac(struct mv88e6xxx_chip *chip, u8 *addr)
+ return err;
+ }
+
++/* Offset 0x1C: QoS Weights Register */
++
++static int mv88e6390_g2_qos_weights_write(struct mv88e6xxx_chip *chip,
++ u8 ptr, u8 data)
++{
++ u16 val = MV88E6XXX_G2_QOS_WEIGHTS_UPDATE |
++ FIELD_PREP(MV88E6390_G2_QOS_WEIGHTS_PTR_MASK, ptr) |
++ FIELD_PREP(MV88E6XXX_G2_QOS_WEIGHTS_DATA_MASK, data);
++
++ return mv88e6xxx_g2_write(chip, MV88E6XXX_G2_QOS_WEIGHTS, val);
++}
++
++/* Load the weighted round robin sequence the queue controller walks on
++ * every port: @len queue numbers, served one frame each in turn. Two
++ * entries per octet, the earlier one in the low bits, and the length
++ * written last since that is what puts the new table in use.
++ */
++int mv88e6390_g2_set_qos_weights(struct mv88e6xxx_chip *chip, const u8 *seq,
++ unsigned int len)
++{
++ unsigned int i;
++ u8 data;
++ int err;
++
++ if (!len || len > MV88E6390_G2_QOS_WEIGHTS_MAX_LEN)
++ return -EINVAL;
++
++ for (i = 0; i < len; i += 2) {
++ data = seq[i] & 0x7;
++ if (i + 1 < len)
++ data |= (seq[i + 1] & 0x7) << 4;
++
++ err = mv88e6390_g2_qos_weights_write(chip, i / 2, data);
++ if (err)
++ return err;
++ }
++
++ return mv88e6390_g2_qos_weights_write(chip,
++ MV88E6390_G2_QOS_WEIGHTS_PTR_LEN,
++ len);
++}
++
+ /* Offset 0x0E: ATU Statistics */
+
+ int mv88e6xxx_g2_atu_stats_set(struct mv88e6xxx_chip *chip, u16 kind, u16 bin)
+diff --git a/drivers/net/dsa/mv88e6xxx/global2.h b/drivers/net/dsa/mv88e6xxx/global2.h
+index 496269c26c50..7d2dda5e1866 100644
+--- a/drivers/net/dsa/mv88e6xxx/global2.h
++++ b/drivers/net/dsa/mv88e6xxx/global2.h
+@@ -266,6 +266,8 @@
+ #define MV88E6XXX_G2_QOS_WEIGHTS_UPDATE 0x8000
+ #define MV88E6352_G2_QOS_WEIGHTS_PTR_MASK 0x3f00
+ #define MV88E6390_G2_QOS_WEIGHTS_PTR_MASK 0x7f00
++#define MV88E6390_G2_QOS_WEIGHTS_PTR_LEN 0x40
++#define MV88E6390_G2_QOS_WEIGHTS_MAX_LEN 128
+ #define MV88E6XXX_G2_QOS_WEIGHTS_DATA_MASK 0x00ff
+
+ /* Offset 0x1D: Misc Register */
+@@ -332,6 +334,9 @@ int mv88e6xxx_g2_smi_phy_write_c45(struct mv88e6xxx_chip *chip,
+ int addr, int devad, int reg, u16 val);
+ int mv88e6xxx_g2_set_switch_mac(struct mv88e6xxx_chip *chip, u8 *addr);
+
++int mv88e6390_g2_set_qos_weights(struct mv88e6xxx_chip *chip, const u8 *seq,
++ unsigned int len);
++
+ int mv88e6xxx_g2_get_eeprom8(struct mv88e6xxx_chip *chip,
+ struct ethtool_eeprom *eeprom, u8 *data);
+ int mv88e6xxx_g2_set_eeprom8(struct mv88e6xxx_chip *chip,
+diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c
+index 47834060cabf..be5aed565faf 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.c
++++ b/drivers/net/dsa/mv88e6xxx/port.c
+@@ -2088,6 +2088,34 @@ int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port)
+ return mv88e6390_port_set_default_prio(chip, port, err);
+ }
+
++/* Offset 0x1C: Port Queue Control */
++
++static int mv88e6390_port_queue_ctl_write(struct mv88e6xxx_chip *chip,
++ int port, u8 ptr, u8 data)
++{
++ u16 reg;
++
++ reg = MV88E6390_PORT_QUEUE_CTL_UPDATE |
++ FIELD_PREP(MV88E6390_PORT_QUEUE_CTL_PTR_MASK, ptr) |
++ FIELD_PREP(MV88E6390_PORT_QUEUE_CTL_DATA_MASK, data);
++
++ return mv88e6xxx_port_write(chip, port, MV88E6390_PORT_QUEUE_CTL, reg);
++}
++
++/* Serve the @strict highest queues in strict priority order and the
++ * rest by weighted round robin, with the weights from Global 2.
++ */
++int mv88e6390_port_set_sched(struct mv88e6xxx_chip *chip, int port,
++ unsigned int strict)
++{
++ u8 mode = min_t(unsigned int, strict,
++ MV88E6390_PORT_QUEUE_CTL_SCHED_STRICT_MASK);
++
++ return mv88e6390_port_queue_ctl_write(chip, port,
++ MV88E6390_PORT_QUEUE_CTL_PTR_SCHED,
++ mode);
++}
++
+ /* Egress remarking uses the frame priority assigned at ingress to look
+ * up the PCP written to tagged frames and the DSCP written to IP frames.
+ * Green and yellow frames have separate tables, both are kept equal as
+diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h
+index 49674351b532..1434174a0937 100644
+--- a/drivers/net/dsa/mv88e6xxx/port.h
++++ b/drivers/net/dsa/mv88e6xxx/port.h
+@@ -502,6 +502,14 @@
+ #define MV88E6390_PORT_CTL3_UPDATE_DSCP 0x0020
+ #define MV88E6390_PORT_CTL3_UPDATE_DEI 0x0008
+
++/* Offset 0x1C: Port Queue Control */
++#define MV88E6390_PORT_QUEUE_CTL 0x1c
++#define MV88E6390_PORT_QUEUE_CTL_UPDATE 0x8000
++#define MV88E6390_PORT_QUEUE_CTL_PTR_MASK 0x7f00
++#define MV88E6390_PORT_QUEUE_CTL_PTR_SCHED 0x00
++#define MV88E6390_PORT_QUEUE_CTL_DATA_MASK 0x00ff
++#define MV88E6390_PORT_QUEUE_CTL_SCHED_STRICT_MASK 0x07
++
+ /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */
+ #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18
+
+@@ -598,6 +606,8 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port,
+ u8 pcp, u8 dei, int prio);
+ int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port);
++int mv88e6390_port_set_sched(struct mv88e6xxx_chip *chip, int port,
++ unsigned int strict);
+ int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port,
+ u64 bps);
+ int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port,
diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am
index 9e4deb8fd8..e04074ce16 100644
--- a/src/confd/src/Makefile.am
+++ b/src/confd/src/Makefile.am
@@ -48,6 +48,7 @@ confd_plugin_la_SOURCES = \
if-vlan.c \
if-gre.c \
if-vxlan.c \
+ if-qos.c \
if-wifi.c \
if-wireguard.c \
if-ppp.c \
diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c
new file mode 100644
index 0000000000..bc4e25bd73
--- /dev/null
+++ b/src/confd/src/if-qos.c
@@ -0,0 +1,855 @@
+/* SPDX-License-Identifier: BSD-3-Clause */
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include "interfaces.h"
+
+#define NUM_PRIO 8
+#define ETS_QUANTUM_UNIT 1514 /* one frame of bytes per percent of share */
+#define MAX_TC 8
+
+/*
+ * IEEE 802.1Q-2022 Table 8-5, recommended priority to traffic class
+ * mappings for an ordinary bridge. Indexed by [num_tc - 1][priority].
+ */
+static const uint8_t tc_ieee[MAX_TC][NUM_PRIO] = {
+ { 0, 0, 0, 0, 0, 0, 0, 0 },
+ { 0, 0, 0, 0, 1, 1, 1, 1 },
+ { 0, 0, 0, 0, 1, 1, 2, 2 },
+ { 0, 0, 1, 1, 2, 2, 3, 3 },
+ { 0, 0, 1, 1, 2, 2, 3, 4 },
+ { 1, 0, 2, 2, 3, 3, 4, 5 },
+ { 1, 0, 2, 3, 4, 4, 5, 6 },
+ { 1, 0, 2, 3, 4, 5, 6, 7 },
+};
+
+/*
+ * IEEE 802.1Q-2022 Table 34-1, priority to traffic class with SR
+ * classes A (priority 3) and B (priority 2) on the two highest classes.
+ */
+static const uint8_t tc_ieee_sr[MAX_TC][NUM_PRIO] = {
+ { 0, 0, 0, 0, 0, 0, 0, 0 },
+ { 0, 0, 1, 1, 0, 0, 0, 0 },
+ { 0, 0, 1, 2, 0, 0, 0, 0 },
+ { 0, 0, 2, 3, 1, 1, 1, 1 },
+ { 0, 0, 3, 4, 1, 1, 2, 2 },
+ { 0, 0, 4, 5, 1, 1, 2, 3 },
+ { 0, 0, 5, 6, 1, 2, 3, 4 },
+ { 1, 0, 6, 7, 2, 3, 4, 5 },
+};
+
+/* RFC 4594 per-hop-behaviour groups to priority; other codepoints untrusted. */
+static const uint8_t dscp_ietf[][2] = {
+ { 0, 0 },
+ { 8, 1 }, { 10, 1 }, { 12, 1 }, { 14, 1 },
+ { 16, 2 }, { 18, 2 }, { 20, 2 }, { 22, 2 },
+ { 24, 3 }, { 26, 3 }, { 28, 3 }, { 30, 3 },
+ { 32, 4 }, { 34, 4 }, { 36, 4 }, { 38, 4 },
+ { 40, 5 }, { 46, 5 },
+ { 48, 6 },
+ { 56, 7 },
+};
+
+enum tsa {
+ TSA_STRICT,
+ TSA_ETS,
+ TSA_UNSUPPORTED,
+};
+
+struct qos_egress {
+ int num_tc;
+ uint8_t map[NUM_PRIO];
+ enum tsa algo[MAX_TC];
+ uint8_t bandwidth[MAX_TC]; /* percent, ETS classes only */
+ uint64_t rate; /* bits/s, 0 = no rate limit */
+ uint32_t burst; /* bytes */
+};
+
+/*
+ * A non-presence container with defaults always exists in the tree; only
+ * a leaf, list entry or presence container that is not a default makes
+ * it configuration.
+ */
+static bool qos_is_explicit(struct lyd_node *node)
+{
+ struct lyd_node *child;
+
+ if (!node || !node->schema)
+ return false;
+
+ switch (node->schema->nodetype) {
+ case LYS_LEAF:
+ case LYS_LEAFLIST:
+ return !(node->flags & LYD_DEFAULT);
+ case LYS_LIST:
+ return true;
+ case LYS_CONTAINER:
+ if (node->schema->flags & LYS_PRESENCE)
+ return true;
+ break;
+ default:
+ break;
+ }
+
+ LY_LIST_FOR(lyd_child(node), child)
+ if (qos_is_explicit(child))
+ return true;
+
+ return false;
+}
+
+/*
+ * Drivers with DCB rewrite support, and the trust orders their dcb
+ * apptrust accepts; the kernel has no query for either. Same table as
+ * yanger's capabilities. The rewrite table cannot be probed by trying:
+ * a driver with dcbnl operations but no dcbnl_setrewr gets the kernel's
+ * generic table, which accepts the entries and programs nothing.
+ */
+static const struct {
+ const char *driver;
+ const char *orders[5];
+ bool pcp_encoded; /* fabric always encodes PCP from the priority */
+} dcb_drivers[] = {
+ { "sparx5-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false },
+ { "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false },
+ { "mv88e6085", { "pcp", "dscp", "pcp-dscp", "dscp-pcp", NULL }, true },
+};
+
+/*
+ * One class per transmit queue, at most eight. A single queue has no
+ * queue structure to respect, so the kernel's eight classes apply.
+ * Interfaces that do not exist yet get eight as well.
+ */
+static int qos_num_tc(const char *ifname)
+{
+ char path[PATH_MAX];
+ struct dirent *d;
+ int n = 0;
+ DIR *dir;
+
+ snprintf(path, sizeof(path), "/sys/class/net/%s/queues", ifname);
+ dir = opendir(path);
+ if (!dir)
+ return MAX_TC;
+
+ while ((d = readdir(dir)))
+ if (!strncmp(d->d_name, "tx-", 3))
+ n++;
+ closedir(dir);
+
+ return n > 1 ? MIN(n, MAX_TC) : MAX_TC;
+}
+
+/* Physical ports get the defaults; virtual interfaces only on request. */
+static bool qos_is_port(const char *ifname)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "/sys/class/net/%s/device", ifname);
+ return access(path, F_OK) == 0;
+}
+
+/* Switch port behind a DSA conduit: the fabric never sees frames the CPU sends. */
+static bool qos_is_dsa(const char *ifname)
+{
+ char path[PATH_MAX], line[128];
+ bool dsa = false;
+ FILE *fp;
+
+ snprintf(path, sizeof(path), "/sys/class/net/%s/uevent", ifname);
+ fp = fopen(path, "r");
+ if (!fp)
+ return false;
+
+ while (fgets(line, sizeof(line), fp))
+ if (!strcmp(chomp(line), "DEVTYPE=dsa"))
+ dsa = true;
+ fclose(fp);
+
+ return dsa;
+}
+
+static const char *qos_driver(const char *ifname, char *buf, size_t len)
+{
+ char path[PATH_MAX], line[128];
+ const char *driver = NULL;
+ FILE *fp;
+
+ snprintf(path, sizeof(path), "/sys/class/net/%s/device/uevent", ifname);
+ fp = fopen(path, "r");
+ if (!fp)
+ return NULL;
+
+ while (fgets(line, sizeof(line), fp)) {
+ if (strncmp(line, "DRIVER=", 7))
+ continue;
+ strlcpy(buf, chomp(line + 7), len);
+ driver = buf;
+ break;
+ }
+ fclose(fp);
+
+ return driver;
+}
+
+static int qos_dcb_driver(const char *ifname)
+{
+ char buf[64];
+ const char *driver = qos_driver(ifname, buf, sizeof(buf));
+ size_t i;
+
+ if (driver)
+ for (i = 0; i < NELEMS(dcb_drivers); i++)
+ if (!strcmp(dcb_drivers[i].driver, driver))
+ return i;
+
+ return -1;
+}
+
+/*
+ * A switch fabric that encodes the PCP from the frame priority on every
+ * port, as an 802.1Q bridge does, is left to it: the tables it boots
+ * with are the identity encoding and are never disabled, so a cascade
+ * of chips, which carries only the priority between them, behaves like
+ * a single chip. remark pcp then changes nothing.
+ */
+static bool qos_pcp_encoded(const char *ifname)
+{
+ int i = qos_dcb_driver(ifname);
+
+ return i >= 0 && dcb_drivers[i].pcp_encoded;
+}
+
+/* Unknown drivers are not limited: without DCB the order is honoured in software. */
+static bool qos_trust_supported(const char *ifname, const char *order)
+{
+ int i = qos_dcb_driver(ifname);
+ size_t j;
+
+ if (i < 0)
+ return true;
+
+ for (j = 0; dcb_drivers[i].orders[j]; j++)
+ if (!strcmp(dcb_drivers[i].orders[j], order))
+ return true;
+
+ return false;
+}
+
+static enum tsa tsa_from_str(const char *val)
+{
+ const char *id = strrchr(val, ':');
+
+ id = id ? id + 1 : val;
+
+ if (!strcmp(id, "strict-priority"))
+ return TSA_STRICT;
+ if (!strcmp(id, "enhanced-transmission-selection"))
+ return TSA_ETS;
+
+ return TSA_UNSUPPORTED;
+}
+
+/* The trust leaf as dcb apptrust order words, none as an empty order. */
+static const char *trust_order(struct lyd_node *ingress)
+{
+ const char *val = ingress ? lydx_get_cattr(ingress, "trust") : NULL;
+
+ if (!val)
+ return "pcp";
+ if (!strcmp(val, "dscp-pcp"))
+ return "dscp pcp";
+ if (!strcmp(val, "pcp-dscp"))
+ return "pcp dscp";
+ if (!strcmp(val, "none"))
+ return "";
+
+ return val;
+}
+
+static int qos_parse_egress(struct lyd_node *egress, const char *ifname, struct qos_egress *eg)
+{
+ struct lyd_node *table, *tc;
+ const char *val;
+ int i;
+
+ memset(eg, 0, sizeof(*eg));
+ eg->num_tc = qos_num_tc(ifname);
+
+ table = egress ? lydx_get_child(egress, "traffic-class-table") : NULL;
+
+ /* Preset, or custom leaves with the ieee preset behind unset ones. */
+ val = table ? lydx_get_cattr(table, "preset") : NULL;
+ memcpy(eg->map, val && !strcmp(val, "ieee-sr") ? tc_ieee_sr[eg->num_tc - 1]
+ : tc_ieee[eg->num_tc - 1], sizeof(eg->map));
+ for (i = 0; i < NUM_PRIO && table; i++) {
+ char name[16];
+
+ snprintf(name, sizeof(name), "priority%d", i);
+ val = lydx_get_cattr(table, name);
+ if (val)
+ eg->map[i] = atoi(val);
+ }
+
+ for (i = 0; i < MAX_TC; i++)
+ eg->algo[i] = TSA_STRICT;
+
+ if (!egress)
+ return 0;
+
+ LYX_LIST_FOR_EACH(lyd_child(egress), tc, "traffic-class") {
+ int id = atoi(lydx_get_cattr(tc, "id"));
+
+ if (id < 0 || id >= MAX_TC)
+ return -EINVAL;
+
+ val = lydx_get_cattr(tc, "algorithm");
+ eg->algo[id] = val ? tsa_from_str(val) : TSA_STRICT;
+
+ val = lydx_get_cattr(tc, "bandwidth");
+ if (val)
+ eg->bandwidth[id] = strtoul(val, NULL, 10);
+ }
+
+ table = lydx_get_child(egress, "rate-limit");
+ if (table) {
+ val = lydx_get_cattr(table, "rate");
+ eg->rate = val ? strtoull(val, NULL, 10) : 0;
+
+ /* Ten milliseconds at rate, never below one frame */
+ val = lydx_get_cattr(table, "burst");
+ eg->burst = val ? strtoul(val, NULL, 10) : eg->rate / 8 / 100;
+ if (eg->burst < 1518)
+ eg->burst = 1518;
+ }
+
+ return 0;
+}
+
+/*
+ * Checks the YANG model cannot express: map values and class ids
+ * against the port's class count, the algorithms in use today, and
+ * the layout tc ets accepts (strict bands first, i.e. the highest
+ * classes).
+ */
+static int qos_validate(sr_session_ctx_t *session, struct lyd_node *cif, struct lyd_node *qos)
+{
+ const char *ifname = lydx_get_cattr(cif, "name");
+ struct lyd_node *egress, *tc;
+ struct qos_egress eg;
+ struct lyd_node *ingress = lydx_get_child(qos, "ingress");
+ const char *trust;
+ bool ets = false;
+ int i;
+
+ trust = ingress ? lydx_get_cattr(ingress, "trust") : NULL;
+ if (trust && strcmp(trust, "none") && !qos_trust_supported(ifname, trust)) {
+ sr_session_set_error_message(session, "%s: trust order %s not supported "
+ "by this port, see qos capabilities", ifname, trust);
+ return -EINVAL;
+ }
+
+ egress = lydx_get_child(qos, "egress");
+ if (qos_parse_egress(egress, ifname, &eg)) {
+ sr_session_set_error_message(session, "%s: invalid traffic class table", ifname);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < NUM_PRIO; i++) {
+ if (eg.map[i] >= eg.num_tc) {
+ sr_session_set_error_message(session, "%s: priority%d maps to traffic "
+ "class %d, port has %d classes", ifname, i,
+ eg.map[i], eg.num_tc);
+ return -EINVAL;
+ }
+ }
+
+ if (!egress)
+ return 0;
+
+ LYX_LIST_FOR_EACH(lyd_child(egress), tc, "traffic-class") {
+ int id = atoi(lydx_get_cattr(tc, "id"));
+
+ if (id >= eg.num_tc) {
+ sr_session_set_error_message(session, "%s: traffic class %d, port has "
+ "%d classes", ifname, id, eg.num_tc);
+ return -EINVAL;
+ }
+
+ if (eg.algo[id] == TSA_UNSUPPORTED) {
+ sr_session_set_error_message(session, "%s: traffic class %d: algorithm %s "
+ "not supported, use strict-priority or "
+ "enhanced-transmission-selection", ifname, id,
+ lydx_get_cattr(tc, "algorithm"));
+ return -EINVAL;
+ }
+ }
+
+ /* Walk from the top: strict classes, then weighted, never back. */
+ for (i = eg.num_tc - 1; i >= 0; i--) {
+ if (eg.algo[i] == TSA_ETS)
+ ets = true;
+ else if (ets) {
+ sr_session_set_error_message(session, "%s: traffic class %d: strict-priority "
+ "classes must be the highest-numbered ones, "
+ "above all weighted classes", ifname, i);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
+enum trust {
+ TRUST_PCP,
+ TRUST_DSCP,
+};
+
+struct qos_ingress {
+ const char *order;
+ int nfields;
+ enum trust field[2];
+ int dflt;
+ int8_t pcp[NUM_PRIO][2]; /* [pcp][dei] to priority, -1 unset */
+ int8_t dscp[64]; /* to priority, -1 untrusted */
+};
+
+static void qos_parse_ingress(struct lyd_node *ingress, struct qos_ingress *in)
+{
+ struct lyd_node *map, *entry;
+ const char *val, *preset, *word;
+ int i;
+
+ memset(in, 0, sizeof(*in));
+ memset(in->pcp, -1, sizeof(in->pcp));
+ memset(in->dscp, -1, sizeof(in->dscp));
+
+ in->order = trust_order(ingress);
+ for (word = in->order; *word; word = *word == ' ' ? word + 1 : word) {
+ in->field[in->nfields++] = strncmp(word, "pcp", 3) ? TRUST_DSCP : TRUST_PCP;
+ word += strcspn(word, " ");
+ }
+
+ if (ingress) {
+ val = lydx_get_cattr(ingress, "default-priority");
+ in->dflt = val ? atoi(val) : 0;
+ }
+
+ /* The one preset is the 802.1Q default decoding, PCP n to priority n. */
+ map = ingress ? lydx_get_child(ingress, "pcp-map") : NULL;
+ if (!map || !lydx_get_child(map, "entry")) {
+ for (i = 0; i < NUM_PRIO; i++)
+ in->pcp[i][0] = in->pcp[i][1] = i;
+ } else {
+ LYX_LIST_FOR_EACH(lyd_child(map), entry, "entry") {
+ int pcp = atoi(lydx_get_cattr(entry, "pcp"));
+ int dei = lydx_get_bool(entry, "dei") ? 1 : 0;
+
+ in->pcp[pcp][dei] = atoi(lydx_get_cattr(entry, "priority"));
+ }
+ }
+
+ map = ingress ? lydx_get_child(ingress, "dscp-map") : NULL;
+ if (!map || !lydx_get_child(map, "entry")) {
+ preset = map ? lydx_get_cattr(map, "preset") : NULL;
+ if (preset && !strcmp(preset, "msb")) {
+ for (i = 0; i < 64; i++)
+ in->dscp[i] = i >> 3;
+ } else {
+ for (i = 0; i < (int)NELEMS(dscp_ietf); i++)
+ in->dscp[dscp_ietf[i][0]] = dscp_ietf[i][1];
+ }
+ } else {
+ LYX_LIST_FOR_EACH(lyd_child(map), entry, "entry") {
+ if (!lydx_get_bool(entry, "trusted"))
+ continue;
+
+ in->dscp[atoi(lydx_get_cattr(entry, "dscp"))] =
+ atoi(lydx_get_cattr(entry, "priority"));
+ }
+ }
+}
+
+/*
+ * One DCB APP table per port, the trust order as dcb apptrust. Drivers
+ * without the operations fail the calls; app_err then selects the
+ * software path below.
+ */
+static void gen_ingress_dcb(FILE *fp, const char *ifname, const struct qos_ingress *in)
+{
+ int i, dei, n;
+
+ fputs("trust_err=0 app_err=0 rewr_err=0\n", fp);
+ fprintf(fp, "dcb apptrust set dev %s order %s 2>/dev/null || trust_err=1\n", ifname, in->order);
+ fprintf(fp, "dcb app flush dev %s default-prio pcp-prio dscp-prio 2>/dev/null || app_err=1\n",
+ ifname);
+ fprintf(fp, "dcb app add dev %s", ifname);
+
+ for (i = 0, n = 0; i < NUM_PRIO; i++)
+ for (dei = 0; dei < 2; dei++)
+ if (in->pcp[i][dei] >= 0)
+ fprintf(fp, "%s %d%s:%d", n++ ? "" : " pcp-prio", i,
+ dei ? "de" : "nd", in->pcp[i][dei]);
+
+ for (i = 0, n = 0; i < 64; i++)
+ if (in->dscp[i] >= 0)
+ fprintf(fp, "%s %d:%d", n++ ? "" : " dscp-prio", i, in->dscp[i]);
+
+ /* default-prio takes every following word as a priority, so it goes last */
+ fprintf(fp, " default-prio %d 2>/dev/null || app_err=1\n", in->dflt);
+}
+
+/*
+ * Software classification: tc flower on a clsact ingress, one block of
+ * rules per trusted field in trust order, then a catch-all for the
+ * default priority. First match wins. flower cannot match DEI, so the
+ * DEI 0 entry is used for both. Tagged IP needs its own rules since the
+ * DSCP then sits behind the VLAN header, and a pref holds one protocol,
+ * so each variant gets its own. A hundred rules per port is normal, so
+ * they go through one tc batch rather than one process each.
+ */
+static void gen_ingress_flower(FILE *fp, const char *ifname, const struct qos_ingress *in)
+{
+ static const char *ipproto[] = {
+ "protocol ip flower",
+ "protocol ipv6 flower",
+ "protocol 802.1Q flower vlan_ethtype ip",
+ "protocol 802.1Q flower vlan_ethtype ipv6",
+ };
+ int f, i, p, pref = 100;
+
+ fputs("tc -force -batch - <nfields; f++, pref += 100) {
+ if (in->field[f] == TRUST_PCP) {
+ for (i = 0; i < NUM_PRIO; i++) {
+ int prio = in->pcp[i][0] >= 0 ? in->pcp[i][0] : in->pcp[i][1];
+
+ if (prio < 0)
+ continue;
+ fprintf(fp, "filter add dev %s ingress pref %d protocol 802.1Q "
+ "flower vlan_prio %d action skbedit priority %d\n",
+ ifname, pref, i, prio);
+ }
+ continue;
+ }
+
+ for (i = 0; i < 64; i++) {
+ if (in->dscp[i] < 0)
+ continue;
+ for (p = 0; p < (int)NELEMS(ipproto); p++)
+ fprintf(fp, "filter add dev %s ingress pref %d %s ip_tos 0x%02x/0xfc "
+ "action skbedit priority %d\n", ifname, pref + p, ipproto[p],
+ i << 2, in->dscp[i]);
+ }
+ }
+
+ fprintf(fp, "filter add dev %s ingress pref 900 matchall action skbedit priority %d\n",
+ ifname, in->dflt);
+ fputs("EOF\n", fp);
+}
+
+static void gen_ingress(FILE *fp, const char *ifname, struct lyd_node *ingress)
+{
+ struct qos_ingress in;
+
+ qos_parse_ingress(ingress, &in);
+ gen_ingress_dcb(fp, ifname, &in);
+
+ fputs("if [ $app_err -ne 0 ]; then\n", fp);
+ gen_ingress_flower(fp, ifname, &in);
+ fputs("fi\n", fp);
+}
+
+/*
+ * Software DSCP remarking: tc basic filters on the egress side matching
+ * the skb priority and the frame's ethertype, rewriting the DS field
+ * with pedit and fixing the IPv4 header checksum. Tagged frames carry
+ * the ethertype behind the tag when the NIC has no VLAN offload, hence
+ * the second pair of rules. PCP has no software counterpart: act_vlan
+ * cannot change the priority without also setting the VLAN ID.
+ */
+static void gen_remark_pedit(FILE *fp, const char *ifname)
+{
+ static const struct {
+ const char *match;
+ const char *munge;
+ const char *csum;
+ } variants[] = {
+ { "cmp(u16 at 12 layer link eq 0x0800)", "ip dsfield", " pipe action csum ip" },
+ { "cmp(u16 at 12 layer link eq 0x86dd)", "ip6 traffic_class", "" },
+ { "cmp(u16 at 12 layer link eq 0x8100) and cmp(u16 at 16 layer link eq 0x0800)",
+ "ip dsfield", " pipe action csum ip" },
+ { "cmp(u16 at 12 layer link eq 0x8100) and cmp(u16 at 16 layer link eq 0x86dd)",
+ "ip6 traffic_class", "" },
+ };
+ int i, v;
+
+ fputs("tc -force -batch - </dev/null || rewr_err=1\n", ifname,
+ encoded ? "" : " prio-pcp");
+ /* One code point per priority, a DEI 1 entry would replace the DEI 0 one */
+ if (!encoded && pcp && !strcmp(pcp, "from-priority")) {
+ fprintf(fp, "dcb rewr add dev %s prio-pcp", ifname);
+ for (i = 0; i < NUM_PRIO; i++)
+ fprintf(fp, " %d:%dnd", i, i);
+ fputs(" 2>/dev/null || rewr_err=1\n", fp);
+ }
+ if (dscp && !strcmp(dscp, "from-priority")) {
+ fprintf(fp, "dcb rewr add dev %s prio-dscp", ifname);
+ for (i = 0; i < NUM_PRIO; i++)
+ fprintf(fp, " %d:%d", i, i << 3);
+ fputs(" 2>/dev/null || rewr_err=1\n", fp);
+ }
+ }
+
+ /*
+ * On a DSA switch the hardware tables only see forwarded frames;
+ * frames the CPU sends are injected past them, so those are
+ * remarked by the kernel on the port's egress as well.
+ */
+ if (dscp && !strcmp(dscp, "from-priority")) {
+ bool dsa = qos_is_dsa(ifname);
+
+ if (!dsa)
+ fputs("if [ $rewr_err -ne 0 ]; then\n", fp);
+ gen_remark_pedit(fp, ifname);
+ if (!dsa)
+ fputs("fi\n", fp);
+ }
+}
+
+/*
+ * Only settings the user configured are reported; the defaults are
+ * applied on every port with a qos container and would flood the log
+ * on hardware without DCB. Software classification honours the trust
+ * order itself, so trust_err only matters when the DCB table was taken.
+ */
+static void gen_dcb_log(FILE *fp, const char *ifname, struct lyd_node *ingress,
+ struct lyd_node *remark)
+{
+ if (qos_is_explicit(ingress)) {
+ fprintf(fp, "[ $app_err -eq 0 ] || logger -t confd -p user.notice "
+ "\"%s: no DCB support in driver, classifying in software\"\n", ifname);
+ fprintf(fp, "[ $trust_err -eq 0 ] || [ $app_err -ne 0 ] || logger -t confd -p user.notice "
+ "\"%s: dcb apptrust unsupported by driver, trust order not applied\"\n",
+ ifname);
+ }
+
+ if (qos_is_explicit(remark)) {
+ const char *pcp = lydx_get_cattr(remark, "pcp");
+
+ fprintf(fp, "[ $rewr_err -eq 0 ] || logger -t confd -p user.notice "
+ "\"%s: no DCB rewrite support in driver, remarking DSCP in software\"\n",
+ ifname);
+ if (pcp && !strcmp(pcp, "from-priority"))
+ fprintf(fp, "[ $rewr_err -eq 0 ] || logger -t confd -p user.notice "
+ "\"%s: PCP remarking needs driver support, not applied\"\n", ifname);
+ }
+}
+
+/*
+ * tc ets carries the whole layout, and a driver that offloads it says
+ * so in the qdisc: band 0 is dequeued first, so class N-1 is band 0,
+ * strict bands come first, and quanta are listed for the weighted
+ * bands in band order, one frame per percent of share so no band gets
+ * less than a frame of credit per round.
+ */
+static void gen_ets(FILE *fp, const char *ifname, struct qos_egress *eg, const char *attach)
+{
+ int i, nstrict = 0;
+
+ for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--)
+ nstrict++;
+
+ fprintf(fp, "qdisc add dev %s %s ets bands %d strict %d", ifname, attach, eg->num_tc, nstrict);
+ if (nstrict < eg->num_tc) {
+ fputs(" quanta", fp);
+ for (i = eg->num_tc - 1 - nstrict; i >= 0; i--)
+ fprintf(fp, " %u", eg->bandwidth[i] * ETS_QUANTUM_UNIT);
+ }
+ fputs(" priomap", fp);
+ for (i = 0; i < NUM_PRIO; i++)
+ fprintf(fp, " %d", eg->num_tc - 1 - eg->map[i]);
+ fputc('\n', fp);
+}
+
+static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg)
+{
+ const char *attach = "root";
+
+ fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname);
+ if (!eg->rate && eg->num_tc < 2)
+ return;
+
+ fputs("tc -force -batch - <rate) {
+ fprintf(fp, "qdisc add dev %s root handle 1: tbf rate %" PRIu64 "bit burst %u"
+ " latency 100ms\n", ifname, eg->rate, eg->burst);
+ attach = "parent 1:1 handle 2:";
+ }
+
+ if (eg->num_tc >= 2)
+ gen_ets(fp, ifname, eg, attach);
+ fputs("EOF\n", fp);
+}
+
+static int gen_reset(struct dagger *net, const char *ifname)
+{
+ FILE *fp;
+
+ fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "qos.sh");
+ if (!fp)
+ return -EIO;
+
+ fputs("tc -force -batch - 2>/dev/null </dev/null <schema)
+ return false;
+
+ switch (node->schema->nodetype) {
+ case LYS_LEAF:
+ case LYS_LEAFLIST:
+ return lydx_get_diff(node, &nd);
+ case LYS_LIST:
+ return true;
+ case LYS_CONTAINER:
+ if (node->schema->flags & LYS_PRESENCE)
+ return true;
+ break;
+ default:
+ break;
+ }
+
+ LY_LIST_FOR(lyd_child(node), child)
+ if (qos_has_change(child))
+ return true;
+
+ return false;
+}
+
+int netdag_gen_qos(sr_session_ctx_t *session, struct dagger *net, struct lyd_node *cif,
+ struct lyd_node *dif)
+{
+ const char *ifname = lydx_get_cattr(cif, "name");
+ struct lyd_node *qos, *dqos, *ingress, *remark;
+ struct qos_egress eg;
+ FILE *fp;
+ int err;
+
+ qos = lydx_get_child(cif, "qos");
+ dqos = lydx_get_child(dif, "qos");
+
+ /*
+ * The defaults are a complete pipeline and apply on every
+ * physical port, at creation and whenever qos changes. Virtual
+ * interfaces get one only when configured; removing it there
+ * resets the interface.
+ */
+ if (!qos_has_change(dqos) && lydx_get_op(dif) != LYDX_OP_CREATE)
+ return 0;
+
+ if (!qos_is_explicit(qos) && !qos_is_port(ifname)) {
+ if (qos_has_change(dqos))
+ return gen_reset(net, ifname);
+ return 0;
+ }
+
+ err = qos_validate(session, cif, qos);
+ if (err)
+ return err;
+
+ err = qos_parse_egress(lydx_get_child(qos, "egress"), ifname, &eg);
+ if (err)
+ return err;
+
+ fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "qos.sh");
+ if (!fp)
+ return -EIO;
+
+ ingress = lydx_get_child(qos, "ingress");
+ remark = lydx_get_descendant(lyd_child(qos), "egress", "remark", NULL);
+
+ fprintf(fp, "tc qdisc del dev %s clsact 2>/dev/null\n", ifname);
+ gen_ingress(fp, ifname, ingress);
+ gen_remark(fp, ifname, remark);
+ gen_dcb_log(fp, ifname, ingress, remark);
+
+ /*
+ * Replacing the root qdisc reprograms the port's queues, which on
+ * offloading hardware drops traffic for a moment. Only do it when
+ * the traffic classes changed, not for an ingress or remark edit.
+ */
+ if (lydx_get_op(dif) == LYDX_OP_CREATE ||
+ qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "traffic-class-table", NULL)) ||
+ qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "traffic-class", NULL)) ||
+ qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "rate-limit", NULL)))
+ gen_egress(fp, ifname, &eg);
+ fclose(fp);
+
+ return 0;
+}
diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c
index ddb5088127..9976b94629 100644
--- a/src/confd/src/interfaces.c
+++ b/src/confd/src/interfaces.c
@@ -815,6 +815,10 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
goto err_close_ip;
}
+ err = netdag_gen_qos(session, net, cif, dif);
+ if (err)
+ goto err_close_ip;
+
/* Set Addresses */
if (!pppd_owned)
err = err ? : netdag_gen_link_mtu(ip, dif);
diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h
index 53e3b784c7..a1f2d9a803 100644
--- a/src/confd/src/interfaces.h
+++ b/src/confd/src/interfaces.h
@@ -186,6 +186,10 @@ int ppp_add_deps(struct lyd_node *cif);
int modem_add_iface(struct lyd_node *cif, struct dagger *net);
int modem_del_iface(struct lyd_node *dif, struct dagger *net);
+/* if-qos.c */
+int netdag_gen_qos(sr_session_ctx_t *session, struct dagger *net, struct lyd_node *cif,
+ struct lyd_node *dif);
+
/* infix-if-wireguard */
int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif);
int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net);
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 5aec171a40..d131f3ee49 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -22,7 +22,7 @@ MODULES=(
"iana-hardware@2018-03-13.yang"
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
"infix-hardware@2026-10-11.yang"
- "ieee802-dot1q-types@2022-10-29.yang"
+ "ieee802-dot1q-types@2023-10-26.yang"
"infix-ip@2026-04-28.yang"
"infix-if-type@2026-10-11.yang"
"infix-routing@2026-10-07.yang"
@@ -42,7 +42,7 @@ MODULES=(
"ieee802-ethernet-phy-type@2025-09-10.yang"
"infix-ethernet-interface@2026-05-21.yang"
"infix-factory-default@2023-06-28.yang"
- "infix-interfaces@2026-10-22.yang -e vlan-filtering"
+ "infix-interfaces@2026-10-22.yang -e vlan-filtering -e rate-limit"
"ietf-crypto-types -e cleartext-symmetric-keys"
"infix-crypto-types@2026-10-27.yang"
"ietf-keystore -e symmetric-keys"
diff --git a/src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang b/src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang
similarity index 60%
rename from src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang
rename to src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang
index b2f7099718..9f436ca594 100644
--- a/src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang
+++ b/src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang
@@ -1,4 +1,5 @@
module ieee802-dot1q-types {
+ yang-version "1.1";
namespace urn:ieee:std:802.1Q:yang:ieee802-dot1q-types;
prefix dot1q-types;
import ietf-yang-types {
@@ -12,19 +13,51 @@ module ieee802-dot1q-types {
Contact: IEEE 802.1 Working Group Chair
Postal: C/O IEEE 802.1 Working Group
- IEEE Standards Association
- 445 Hoes Lane
- Piscataway, NJ 08854
- USA
+ IEEE Standards Association
+ 445 Hoes Lane
+ Piscataway, NJ 08854
+ USA
E-mail: stds-802-1-chairs@ieee.org";
description
"Common types used within dot1Q-bridge modules.
- Copyright (C) IEEE (2022).
+ Copyright (C) IEEE (2023).
This version of this YANG module is part of IEEE Std 802.1Q; see the
- standard itself for full legal notices.";
+ standard itself for full legal notices.";
+ revision 2023-10-26 {
+ description
+ "Published as part of IEEE Std 802.1Qcj-2023. This revision adds
+ the isid-type.
+
+ The following reference statement identifies each referenced IEEE
+ Standard as updated by applicable amendments.";
+ reference
+ "IEEE Std 802.1Q Bridges and Bridged Networks:
+ IEEE Std 802.1Q-2022, IEEE Std 802.1Qcz-2023, IEEE Std 802.1Qcw-2023,
+ IEEE Std 802.1Qcj-2023.
+ IEEE Std 802 Overview and Architecture: IEEE Std 802-2014.";
+ }
+ revision 2023-10-22 {
+ description
+ "Published as part of IEEE Std 802.1Qcw-2023.
+
+ The following reference statement identifies each referenced IEEE
+ Standard as updated by applicable amendments.";
+ reference
+ "IEEE Std 802.1Q Bridges and Bridged Networks:
+ IEEE Std 802.1Q-2022, IEEE Std 802.1Qcz-2023, IEEE Std 802.1Qcw-2023.
+ IEEE Std 802 Overview and Architecture:
+ IEEE Std 802-2014.";
+ }
+ revision 2023-07-03 {
+ description
+ "Published as part of IEEE Std 802.1Qcz-2023.";
+ reference
+ "IEEE Std 802.1Qcz-2023, Bridges and Bridged Networks - Congestion
+ Isolation.";
+ }
revision 2022-10-29 {
description
"Published as part of IEEE Std 802.1Q-2022.";
@@ -33,28 +66,27 @@ module ieee802-dot1q-types {
}
revision 2020-06-04 {
description
- "Published as part of IEEE Std 802.1Qcx-2020. Second version.";
+ "Published as part of IEEE Std 802.1Qcx-2020.";
reference
"IEEE Std 802.1Qcx-2020, Bridges and Bridged Networks - YANG Data
Model for Connectivity Fault Management.";
}
revision 2018-03-07 {
description
- "Published as part of IEEE Std 802.1Q-2018. Initial version.";
+ "Published as part of IEEE Std 802.1Q-2018.";
reference
"IEEE Std 802.1Q-2018, Bridges and Bridged Networks.";
}
identity dot1q-vlan-type {
description
- "Base identity from which all 802.1Q VLAN tag types are derived
- from.";
+ "Base identity from which all 802.1Q VLAN tag types are derived.";
}
identity c-vlan {
base dot1q-vlan-type;
description
- "An 802.1Q Customer VLAN, using the 81-00 EtherType";
+ "An 802.1Q Customer VLAN, using the 81-00 EtherType.";
reference
- "5.5 of IEEE Std 802.1Q-2022";
+ "5.5 of IEEE Std 802.1Q";
}
identity s-vlan {
base dot1q-vlan-type;
@@ -62,48 +94,51 @@ module ieee802-dot1q-types {
"An 802.1Q Service VLAN, using the 88-A8 EtherType originally
introduced in 802.1ad, and incorporated into 802.1Q (2011)";
reference
- "5.6 of IEEE Std 802.1Q-2022";
+ "5.6 of IEEE Std 802.1Q";
}
identity transmission-selection-algorithm {
description
- "Specify the transmission selection algorithms of IEEE Std
- 802.1Q-2022 Table 8-6";
+ "Specify the transmission selection algorithms of IEEE Std 802.1Q
+ Table 8-6";
}
identity strict-priority {
base transmission-selection-algorithm;
description
"Indicates the strict priority transmission selection algorithm.";
reference
- "Table 8-6 of IEEE Std 802.1Q-2022";
+ "Table 8-6 of IEEE Std 802.1Q";
}
identity credit-based-shaper {
base transmission-selection-algorithm;
description
- "Indicates the credit based shaper transmission selection
- algorithm.";
+ "Indicates the credit-based shaper transmission selection algorithm.";
reference
- "Table 8-6 of IEEE Std 802.1Q-2022";
+ "Table 8-6 of IEEE Std 802.1Q";
}
identity enhanced-transmission-selection {
base transmission-selection-algorithm;
description
"Indicates the enhanced transmission selection algorithm.";
reference
- "Table 8-6 of IEEE Std 802.1Q-2022";
+ "Table 8-6 of IEEE Std 802.1Q";
}
identity asynchronous-traffic-shaping {
base transmission-selection-algorithm;
description
"Indicates the asynchronous transmission selection algorithm.";
reference
- "Table 8-6 of IEEE Std 802.1Q-2022";
+ "Table 8-6 of IEEE Std 802.1Q";
}
identity vendor-specific {
base transmission-selection-algorithm;
description
"Indicates a vendor specific transmission selection algorithm.";
reference
- "Table 8-6 of IEEE Std 802.1Q-2022";
+ "Table 8-6 of IEEE Std 802.1Q";
+ }
+ identity type-of-operation {
+ description
+ "Represents the operation type (name).";
}
typedef name-type {
type string {
@@ -129,9 +164,17 @@ module ieee802-dot1q-types {
"A range of priorities from 0 to 7 (inclusive). The Priority Code
Point (PCP) is a 3-bit field that refers to the class of service
associated with an 802.1Q VLAN tagged frame. The field specifies a
- priority value between 0 and 7, these values can be used by
- quality of service (QoS) to prioritize different classes of
- traffic.";
+ priority value between 0 and 7, these values can be used by quality of
+ service (QoS) to prioritize different classes of traffic.";
+ }
+ typedef num-traffic-class-type {
+ type uint8 {
+ range "1..8";
+ }
+ description
+ "The number of traffic classes supported or participating in a
+ particular feature. There are between 1 and 8 supported traffic
+ classes defined by IEEE Std 802.1Q.";
}
typedef vid-range-type {
type string {
@@ -142,13 +185,12 @@ module ieee802-dot1q-types {
"(,[1-9][0-9]{0,3}(-[1-9][0-9]{0,3})?)*)";
}
description
- "A list of VLAN Ids, or non overlapping VLAN ranges, in ascending
+ "A list of VLAN IDs, or non overlapping VLAN ranges, in ascending
order, between 1 and 4094.
- This type is used to match an ordered list of VLAN Ids, or
- contiguous ranges of VLAN Ids. Valid VLAN Ids must be in the range
- 1 to 4094, and included in the list in non overlapping ascending
- order.
+ This type is used to match an ordered list of VLAN IDs, or contiguous
+ ranges of VLAN IDs. Valid VLAN IDs must be in the range 1 to 4094, and
+ included in the list in non overlapping ascending order.
For example: 1,10-100,250,500-1000";
}
@@ -160,23 +202,21 @@ module ieee802-dot1q-types {
"The vlanid type uniquely identifies a VLAN. This is the 12-bit
VLAN-ID used in the VLAN Tag header. The range is defined by the
referenced specification. This type is in the value set and its
- semantics equivalent to the VlanId textual convention of the
- SMIv2.";
+ semantics equivalent to the VlanId textual convention of the SMIv2.";
}
typedef vlan-index-type {
type uint32 {
range "1..4094 | 4096..4294967295";
}
description
- "A value used to index per-VLAN tables. Values of 0 and 4095 are
- not permitted. The range of valid VLAN indices. If the value is
- greater than 4095, then it represents a VLAN with scope local to
- the particular agent, i.e., one without a global VLAN-ID assigned
- to it. Such VLANs are outside the scope of IEEE 802.1Q, but it is
- convenient to be able to manage them in the same way using this
- YANG module.";
+ "A value used to index per-VLAN tables. Values of 0 and 4095 are not
+ permitted. The range of valid VLAN indices. If the value is greater
+ than 4095, then it represents a VLAN with scope local to the
+ particular agent, i.e., one without a global VLAN-ID assigned to it.
+ Such VLANs are outside the scope of IEEE 802.1Q, but it is convenient
+ to be able to manage them in the same way using this YANG module.";
reference
- "9.6 of IEEE Std 802.1Q-2022";
+ "9.6 of IEEE Std 802.1Q";
}
typedef mstid-type {
type uint32 {
@@ -186,7 +226,7 @@ module ieee802-dot1q-types {
"In an MSTP Bridge, an MSTID, i.e., a value used to identify a
spanning tree (or MST) instance";
reference
- "13.8 of IEEE Std 802.1Q-2022";
+ "13.8 of IEEE Std 802.1Q";
}
typedef pcp-selection-type {
type enumeration {
@@ -210,7 +250,7 @@ module ieee802-dot1q-types {
description
"Priority Code Point selection types.";
reference
- "12.6.2.5.3, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.5.3, 6.9.3 of IEEE Std 802.1Q";
}
typedef protocol-frame-format-type {
type enumeration {
@@ -238,7 +278,7 @@ module ieee802-dot1q-types {
description
"A value representing the frame format to be matched.";
reference
- "12.10.1.7.1 of IEEE Std 802.1Q-2022";
+ "12.10.1.7.1 of IEEE Std 802.1Q";
}
typedef ethertype-type {
type string {
@@ -248,7 +288,7 @@ module ieee802-dot1q-types {
"The EtherType value represented in the canonical order defined by
IEEE 802. The canonical representation uses uppercase characters.";
reference
- "9.2 of IEEE Std 802-2014";
+ "9.2 of IEEE Std 802";
}
typedef dot1q-tag-type {
type identityref {
@@ -257,7 +297,7 @@ module ieee802-dot1q-types {
description
"Identifies a specific 802.1Q tag type";
reference
- "9.5 IEEE Std 802.1Q-2022";
+ "9.5 IEEE Std 802.1Q";
}
typedef traffic-class-type {
type uint8 {
@@ -268,12 +308,25 @@ module ieee802-dot1q-types {
Bridge. Larger values are associated with higher priority traffic
classes.";
reference
- "3.273 of IEEE Std 802.1Q-2022";
+ "3.273 of IEEE Std 802.1Q";
+ }
+ typedef isid-type {
+ type uint32 {
+ range "1|255..16777214";
+ }
+ description
+ "The i-sid type represents a Backbone Service Instance Identifier
+ (I-SID). This is the 24-bit I-SID field used in the I-TAG TCI of a
+ Provider Backbone Bridging frame. The values 0, 2-254, and 16777215
+ are reserved for future standardization. The value 255 is dedicated
+ to use by the SPB protocol.";
+ reference
+ "3.108, 9.7 of IEEE Std 802.1Q-2022";
}
grouping dot1q-tag-classifier-grouping {
description
"A grouping which represents an 802.1Q VLAN, matching both the
- EtherType and a single VLAN Id.";
+ EtherType and a single VLAN ID.";
leaf tag-type {
type dot1q-tag-type;
mandatory true;
@@ -284,13 +337,13 @@ module ieee802-dot1q-types {
type vlanid;
mandatory true;
description
- "VLAN Id";
+ "VLAN ID";
}
}
grouping dot1q-tag-or-any-classifier-grouping {
description
"A grouping which represents an 802.1Q VLAN, matching both the
- EtherType and a single VLAN Id or 'any' to match on any VLAN Id.";
+ EtherType and a single VLAN ID or 'any' to match on any VLAN ID.";
leaf tag-type {
type dot1q-tag-type;
mandatory true;
@@ -304,20 +357,20 @@ module ieee802-dot1q-types {
enum any {
value 4095;
description
- "Matches 'any' VLAN in the range 1 to 4094 that is not
- matched by a more specific VLAN Id match";
+ "Matches 'any' VLAN in the range 1 to 4094 that is not matched
+ by a more specific VLAN ID match";
}
}
}
mandatory true;
description
- "VLAN Id or any";
+ "VLAN ID or any";
}
}
grouping dot1q-tag-ranges-classifier-grouping {
description
- "A grouping which represents an 802.1Q VLAN that matches a range
- of VLAN Ids.";
+ "A grouping which represents an 802.1Q VLAN that matches a range of
+ VLAN IDs.";
leaf tag-type {
type dot1q-tag-type;
mandatory true;
@@ -328,14 +381,14 @@ module ieee802-dot1q-types {
type vid-range-type;
mandatory true;
description
- "VLAN Ids";
+ "VLAN IDs";
}
}
grouping dot1q-tag-ranges-or-any-classifier-grouping {
description
"A grouping which represents an 802.1Q VLAN, matching both the
- EtherType and a single VLAN Id, ordered list of ranges, or 'any'
- to match on any VLAN Id.";
+ EtherType and a single VLAN ID, ordered list of ranges, or 'any' to
+ match on any VLAN ID.";
leaf tag-type {
type dot1q-tag-type;
mandatory true;
@@ -355,23 +408,22 @@ module ieee802-dot1q-types {
}
mandatory true;
description
- "VLAN Ids or any";
+ "VLAN IDs or any";
}
}
grouping priority-regeneration-table-grouping {
description
- "The priority regeneration table provides the ability to map
- incoming priority values on a per-Port basis, under management
- control.";
+ "The priority regeneration table provides the ability to map incoming
+ priority values on a per-Port basis, under management control.";
reference
- "6.9.4 of IEEE Std 802.1Q-2022";
+ "6.9.4 of IEEE Std 802.1Q";
leaf priority0 {
type priority-type;
default "0";
description
"Priority 0";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority1 {
type priority-type;
@@ -379,7 +431,7 @@ module ieee802-dot1q-types {
description
"Priority 1";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority2 {
type priority-type;
@@ -387,7 +439,7 @@ module ieee802-dot1q-types {
description
"Priority 2";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority3 {
type priority-type;
@@ -395,7 +447,7 @@ module ieee802-dot1q-types {
description
"Priority 3";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority4 {
type priority-type;
@@ -403,7 +455,7 @@ module ieee802-dot1q-types {
description
"Priority 4";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority5 {
type priority-type;
@@ -411,7 +463,7 @@ module ieee802-dot1q-types {
description
"Priority 5";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority6 {
type priority-type;
@@ -419,7 +471,7 @@ module ieee802-dot1q-types {
description
"Priority 6";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
leaf priority7 {
type priority-type;
@@ -427,53 +479,53 @@ module ieee802-dot1q-types {
description
"Priority 7";
reference
- "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022";
+ "12.6.2.3, 6.9.4 of IEEE Std 802.1Q";
}
}
grouping pcp-decoding-table-grouping {
description
- "The Priority Code Point decoding table enables the decoding of
- the priority and drop-eligible parameters from the PCP.";
+ "The Priority Code Point decoding table enables the decoding of the
+ priority and drop-eligible parameters from the PCP.";
reference
- "6.9.3 of IEEE Std 802.1Q-2022";
+ "6.9.3 of IEEE Std 802.1Q";
list pcp-decoding-map {
key "pcp";
description
- "This map associates the priority code point field found in the
- VLAN to a priority and drop eligible value based upon the
- priority code point selection type.";
+ "This map associates the priority code point field found in the VLAN
+ with a priority and drop eligible value based upon the priority code
+ point selection type.";
leaf pcp {
type pcp-selection-type;
description
"The priority code point selection type.";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
list priority-map {
key "priority-code-point";
description
- "This map associated a priority code point value to priority
+ "This map associates a priority code point value with priority
and drop eligible parameters.";
leaf priority-code-point {
type priority-type;
description
"Priority associated with the pcp.";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
leaf priority {
type priority-type;
description
"Priority associated with the pcp.";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
leaf drop-eligible {
type boolean;
description
"Drop eligible value for pcp";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
}
}
@@ -483,62 +535,62 @@ module ieee802-dot1q-types {
"The Priority Code Point encoding table encodes the priority and
drop-eligible parameters in the PCP field of the VLAN tag.";
reference
- "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.9, 6.9.3 of IEEE Std 802.1Q";
list pcp-encoding-map {
key "pcp";
description
- "This map associated the priority and drop-eligible parameters
- to the priority used to encode the PCP of the VLAN based upon
+ "This map associates the priority and drop-eligible parameters
+ with the priority used to encode the PCP of the VLAN based upon
the priority code point selection type.";
leaf pcp {
type pcp-selection-type;
description
"The priority code point selection type.";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
list priority-map {
key "priority dei";
description
- "This map associated the priority and drop-eligible parameters
- to the priority code point field of the VLAN tag.";
+ "This map associates the priority and drop-eligible parameters
+ with the priority code point field of the VLAN tag.";
leaf priority {
type priority-type;
description
"Priority associated with the pcp.";
reference
- "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.7, 6.9.3 of IEEE Std 802.1Q";
}
leaf dei {
type boolean;
description
"The drop eligible value.";
reference
- "12.6.2, 8.6.6 of IEEE Std 802.1Q-2022";
+ "12.6.2, 8.6.6 of IEEE Std 802.1Q";
}
leaf priority-code-point {
type priority-type;
description
"PCP value for priority when DEI value";
reference
- "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022";
+ "12.6.2.9, 6.9.3 of IEEE Std 802.1Q";
}
}
}
}
grouping service-access-priority-table-grouping {
description
- "The Service Access Priority Table associates a received priority
- with a service access priority.";
+ "The Service Access Priority Table associates a received priority with
+ a service access priority.";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
leaf priority0 {
type priority-type;
default "0";
description
"Service access priority value for priority 0";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority1 {
type priority-type;
@@ -546,7 +598,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 1";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority2 {
type priority-type;
@@ -554,7 +606,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 2";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority3 {
type priority-type;
@@ -562,7 +614,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 3";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority4 {
type priority-type;
@@ -570,7 +622,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 4";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority5 {
type priority-type;
@@ -578,7 +630,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 5";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority6 {
type priority-type;
@@ -586,7 +638,7 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 6";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
leaf priority7 {
type priority-type;
@@ -594,34 +646,40 @@ module ieee802-dot1q-types {
description
"Service access priority value for priority 7";
reference
- "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022";
+ "12.6.2.17, 6.13.1 of IEEE Std 802.1Q";
}
}
grouping traffic-class-table-grouping {
description
"The Traffic Class Table models the operations that can be
- performed on, or inquire about, the current contents of the
+ performed on, or can inquire about, the current contents of the
Traffic Class Table (8.6.6) for a given Port.";
reference
- "12.6.3, 8.6.6 of IEEE Std 802.1Q-2022";
+ "12.6.3, 8.6.6 of IEEE Std 802.1Q";
list traffic-class-map {
key "priority";
+ status "deprecated";
description
- "The priority index into the traffic class table.";
+ "The priority index into the traffic class table. This list modeled
+ the Traffic Class Table incorrectly. available-traffic-class should
+ be a single value per port and not a list of all possible
+ available-traffic-class. For more information see maintenance issue
+ 0230. The status of this object is deprecated. It is replaced by
+ sibling container traffic-class-table.";
leaf priority {
type priority-type;
description
"The priority of the traffic class entry.";
reference
- "8.6.6 of IEEE Std 802.1Q-2022";
+ "8.6.6 of IEEE Std 802.1Q";
}
list available-traffic-class {
key "num-traffic-class";
description
- "The traffic class index associated with a given priority
- within the traffic class table.";
+ "The traffic class index associated with a given priority within
+ the traffic class table.";
reference
- "8.6.6 of IEEE Std 802.1Q-2022";
+ "8.6.6 of IEEE Std 802.1Q";
leaf num-traffic-class {
type uint8 {
range "1..8";
@@ -629,38 +687,124 @@ module ieee802-dot1q-types {
description
"The available number of traffic classes.";
reference
- "8.6.6 of IEEE Std 802.1Q-2022";
+ "8.6.6 of IEEE Std 802.1Q";
}
leaf traffic-class {
type traffic-class-type;
description
- "The traffic class index associated with a given traffic
- class entry.";
+ "The traffic class index associated with a given traffic class
+ entry.";
reference
- "8.6.6 of IEEE Std 802.1Q-2022";
+ "8.6.6 of IEEE Std 802.1Q";
}
}
}
+ container traffic-class-table {
+ description
+ "The priority to traffic class mapping.";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ leaf number-of-traffic-classes {
+ type uint8 {
+ range "1..8";
+ }
+ description
+ "The number of egress traffic classes supported on this port. This
+ object may be optionally read-only.";
+ reference
+ "12.6.3.1 of IEEE Std 802.1Q";
+ }
+ leaf priority0 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 0";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority1 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 1";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority2 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 2";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority3 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 3";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority4 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 4";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority5 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 5";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority6 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 6";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ leaf priority7 {
+ type traffic-class-type;
+ must
+ "current() < ../number-of-traffic-classes";
+ description
+ "The traffic class index associated with priority 7";
+ reference
+ "8.6.6 of IEEE Std 802.1Q";
+ }
+ }
}
grouping transmission-selection-table-grouping {
description
- "The Transmission Selection Algorithm Table models the operations
- that can be performed on, or inquire about, the current contents
- of the Transmission Selection Algorithm Table (12.20.2) for a
- given Port.";
+ "The Transmission Selection Algorithm Table models the operations that
+ can be performed on, or can inquire about, the current contents of the
+ Transmission Selection Algorithm Table (12.20.2) for a given Port.";
reference
- "12.20.2, 8.6.8 of IEEE Std 802.1Q-2022";
+ "12.20.2, 8.6.8 of IEEE Std 802.1Q";
list transmission-selection-algorithm-map {
key "traffic-class";
description
- "The traffic class to index into the transmission selection
- table.";
+ "The traffic class to index into the transmission selection table.";
leaf traffic-class {
type traffic-class-type;
description
"The traffic class of the entry.";
reference
- "8.6.6 of IEEE Std 802.1Q-2022";
+ "8.6.6 of IEEE Std 802.1Q";
}
leaf transmission-selection-algorithm {
type identityref {
@@ -669,7 +813,7 @@ module ieee802-dot1q-types {
description
"Transmission selection algorithm";
reference
- "8.6.8, Table 8-6 of IEEE Std 802.1Q-2022";
+ "8.6.8, Table 8-6 of IEEE Std 802.1Q";
}
}
}
@@ -678,7 +822,7 @@ module ieee802-dot1q-types {
"A set of control indicators, one for each Port. A Port Map,
containing a control element for each outbound Port";
reference
- "8.8.1, 8.8.2 of IEEE Std 802.1Q-2022";
+ "8.8.1, 8.8.2 of IEEE Std 802.1Q";
list port-map {
key "port-ref";
description
@@ -688,7 +832,7 @@ module ieee802-dot1q-types {
description
"The interface port reference associated with this map.";
reference
- "8.8.1 of IEEE Std 802.1Q-2022";
+ "8.8.1 of IEEE Std 802.1Q";
}
choice map-type {
description
@@ -710,30 +854,29 @@ module ieee802-dot1q-types {
}
enum forward-filter {
description
- "Forwarded or filtered on the basis of dynamic
- filtering information, or on the basis of the default
- Group filtering behavior for the outbound Port (8.8.6)
- if no dynamic filtering information is present
- specifically for the MAC address.";
+ "Forwarded or filtered on the basis of dynamic filtering
+ information, or on the basis of the default Group
+ filtering behavior for the outbound Port (8.8.6) if no
+ dynamic filtering information is present specifically for
+ the MAC address.";
}
}
description
- "containing a control element for each outbound Port,
- specifying that a frame with a destination MAC address,
- and in the case of VLAN Bridge components, VID that meets
- this specification.";
+ "A control element for each outbound Port, specifying that
+ a frame with a destination MAC address, and in the case of
+ VLAN Bridge components, VID that meets this specification.";
reference
- "8.8.1 of IEEE Std 802.1Q-2022";
+ "8.8.1 of IEEE Std 802.1Q";
}
leaf connection-identifier {
type port-number-type;
description
- "A Port MAP may contain a connection identifier (8.8.12)
- for each outbound port. The connection identifier may be
- associated with the Bridge Port value maintained in a
- Dynamic Filtering Entry of the FDB for Bridge Ports.";
+ "A Port MAP may contain a connection identifier (8.8.12) for
+ each outbound port. The connection identifier may be
+ associated with the Bridge Port value maintained in a Dynamic
+ Filtering Entry of the FDB for Bridge Ports.";
reference
- "8.8.1, 8.8.12 of IEEE Std 802.1Q-2022";
+ "8.8.1, 8.8.12 of IEEE Std 802.1Q";
}
}
container static-vlan-registration-entries {
@@ -759,10 +902,10 @@ module ieee802-dot1q-types {
}
}
description
- "The Registrar Administrative Control values for MVRP and
- MIRP for the VID.";
+ "The Registrar Administrative Control values for MVRP and MIRP
+ for the VID.";
reference
- "8.8.2 of IEEE Std 802.1Q-2022";
+ "8.8.2 of IEEE Std 802.1Q";
}
leaf vlan-transmitted {
type enumeration {
@@ -779,7 +922,7 @@ module ieee802-dot1q-types {
"Whether frames are to be VLAN-tagged or untagged when
transmitted.";
reference
- "8.8.2 of IEEE Std 802.1Q-2022";
+ "8.8.2 of IEEE Std 802.1Q";
}
}
container mac-address-registration-entries {
@@ -799,12 +942,11 @@ module ieee802-dot1q-types {
}
}
description
- "containing a control element for each outbound Port,
- specifying that a frame with a destination MAC address,
- and in the case of VLAN Bridge components, VID that meets
- this specification.";
+ "A control element for each outbound Port, specifying that
+ a frame with a destination MAC address, and in the case of
+ VLAN Bridge components, VID that meets this specification.";
reference
- "8.8.4 of IEEE Std 802.1Q-2022";
+ "8.8.4 of IEEE Std 802.1Q";
}
}
container dynamic-vlan-registration-entries {
@@ -819,12 +961,11 @@ module ieee802-dot1q-types {
}
}
description
- "containing a control element for each outbound Port,
- specifying that a frame with a destination MAC address,
- and in the case of VLAN Bridge components, VID that meets
- this specification.";
+ "A control element for each outbound Port, specifying that
+ a frame with a destination MAC address, and in the case of
+ VLAN Bridge components, VID that meets this specification.";
reference
- "8.8.5 of IEEE Std 802.1Q-2022";
+ "8.8.5 of IEEE Std 802.1Q";
}
}
container dynamic-reservation-entries {
@@ -844,12 +985,11 @@ module ieee802-dot1q-types {
}
}
description
- "Containing a control element for each outbound Port,
- specifying that a frame with a destination MAC address,
- and in the case of VLAN Bridge components, VID that meets
- this specification.";
+ "A control element for each outbound Port, specifying that
+ a frame with a destination MAC address, and in the case of
+ VLAN Bridge components, VID that meets this specification.";
reference
- "8.8.7 of IEEE Std 802.1Q-2022";
+ "8.8.7 of IEEE Std 802.1Q";
}
}
container dynamic-filtering-entries {
@@ -864,12 +1004,11 @@ module ieee802-dot1q-types {
}
}
description
- "Containing a control element for each outbound Port,
- specifying that a frame with a destination MAC address,
- and in the case of VLAN Bridge components, VID that meets
- this specification.";
+ "A control element for each outbound Port, specifying that
+ a frame with a destination MAC address, and in the case of
+ VLAN Bridge components, VID that meets this specification.";
reference
- "8.8.3 of IEEE Std 802.1Q-2022";
+ "8.8.3 of IEEE Std 802.1Q";
}
}
}
@@ -879,7 +1018,7 @@ module ieee802-dot1q-types {
description
"Grouping of bridge port statistics.";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
leaf delay-exceeded-discards {
type yang:counter64;
description
@@ -887,7 +1026,7 @@ module ieee802-dot1q-types {
transit delay through the Bridge. It is incremented by both
transparent and source route Bridges.";
reference
- "12.6.1.1.3, 8.6.6 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3, 8.6.6 of IEEE Std 802.1Q";
}
leaf mtu-exceeded-discards {
type yang:counter64;
@@ -896,59 +1035,57 @@ module ieee802-dot1q-types {
size. It is incremented by both transparent and source route
Bridges.";
reference
- "Item g) in 12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "Item g) in 12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf frame-rx {
type yang:counter64;
description
- "The number of frames that have been received by this port from
- its segment. Note that a frame received on the interface
- corresponding to this port is only counted by this object if and
- only if it is for a protocol being processed by the local
- bridging function, including Bridge management frames.";
+ "The number of frames that have been received by this port from its
+ segment. Note that a frame received on the interface corresponding
+ to this port is only counted by this object if and only if it is for
+ a protocol being processed by the local bridging function, including
+ Bridge management frames.";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf octets-rx {
type yang:counter64;
description
- "The total number of octets in all valid frames received
- (including BPDUs, frames addressed to the Bridge as an end
- station, and frames that were submitted to the Forwarding
- Process).";
+ "The total number of octets in all valid frames received (including
+ BPDUs, frames addressed to the Bridge as an end station, and frames
+ that were submitted to the Forwarding Process).";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf frame-tx {
type yang:counter64;
description
- "The number of frames that have been transmitted by this port to
- its segment. Note that a frame transmitted on the interface
+ "The number of frames that have been transmitted by this port to its
+ segment. Note that a frame transmitted on the interface
corresponding to this port is only counted by this object if and
- only if it is for a protocol being processed by the local
- bridging function, including Bridge management frames.";
+ only if it is for a protocol being processed by the local bridging
+ function, including Bridge management frames.";
}
leaf octets-tx {
type yang:counter64;
description
- "The total number of octets that have been transmitted by this
- port to its segment.";
+ "The total number of octets that have been transmitted by this port
+ to its segment.";
}
leaf discard-inbound {
type yang:counter64;
description
- "Count of received valid frames that were discarded (i.e.,
- filtered) by the Forwarding Process.";
+ "Count of received valid frames that were discarded (i.e., filtered)
+ by the Forwarding Process.";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf forward-outbound {
type yang:counter64;
description
- "The number of frames forwarded to the associated MAC Entity
- (8.5).";
+ "The number of frames forwarded to the associated MAC Entity (8.5).";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf discard-lack-of-buffers {
type yang:counter64;
@@ -956,7 +1093,7 @@ module ieee802-dot1q-types {
"The count of frames that were to be transmitted through the
associated Port but were discarded due to lack of buffers.";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf discard-transit-delay-exceeded {
type yang:counter64;
@@ -965,16 +1102,48 @@ module ieee802-dot1q-types {
transit delay through the Bridge. It is incremented by both
transparent and source route Bridges.";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
}
leaf discard-on-error {
type yang:counter64;
description
- "The number of frames that were to be forwarded on the
- associated MAC but could not be transmitted (e.g., frame would
- be too large, 6.5.8).";
+ "The number of frames that were to be forwarded on the associated
+ MAC but could not be transmitted (e.g., frame would be too large,
+ 6.5.8).";
reference
- "12.6.1.1.3 of IEEE Std 802.1Q-2022";
+ "12.6.1.1.3 of IEEE Std 802.1Q";
+ }
+ }
+ grouping base-gate-control-entries {
+ description
+ "Grouping of gate control entries.";
+ list gate-control-entry {
+ key "index";
+ description
+ "List of gate control entries.";
+ leaf index {
+ type uint32;
+ description
+ "The index for this entry.";
+ }
+ leaf operation-name {
+ type identityref {
+ base type-of-operation;
+ }
+ mandatory true;
+ description
+ "The name (type) of the operation for this entry.";
+ }
+ leaf time-interval-value {
+ type uint32;
+ description
+ "timeIntervalValue is a 32-bit unsigned integer, representing a
+ number of nanoseconds. After timeIntervalValue nanoseconds have
+ elapsed since the completion of the previous entry in the gate
+ control list, control passes to the next entry.";
+ reference
+ "12.29.1.2.3, 8.6.8.4 of IEEE Std 802.1Q";
+ }
}
}
}
diff --git a/src/confd/yang/confd/infix-if-qos.yang b/src/confd/yang/confd/infix-if-qos.yang
new file mode 100644
index 0000000000..c7f3aef1c2
--- /dev/null
+++ b/src/confd/yang/confd/infix-if-qos.yang
@@ -0,0 +1,532 @@
+submodule infix-if-qos {
+ yang-version 1.1;
+ belongs-to infix-interfaces {
+ prefix infix-if;
+ }
+
+ import ietf-interfaces {
+ prefix if;
+ }
+ import ietf-inet-types {
+ prefix inet;
+ }
+ import ieee802-dot1q-types {
+ prefix dot1q-types;
+ }
+
+ organization "KernelKit";
+ contact "kernelkit@googlegroups.com";
+ description "Per-interface Quality of Service.
+
+ Ingress classification to a (priority, drop precedence) pair,
+ priority-to-traffic-class binding, per-traffic-class transmission
+ selection, and egress remarking.
+
+ Applies to every interface type, with no dependency on a bridge
+ model. IEEE 802.1Q names and types are used where 802.1Q defines
+ the concept.
+
+ CBS, TAS, frame preemption, and PSFP come from the IEEE 802.1DC
+ modules. VLAN sub-interface PCP mapping stays in
+ infix-if-vlan.yang.";
+
+ revision 2026-09-07 {
+ description "Initial revision: ingress classification, traffic-class
+ table, transmission selection, and egress remarking.";
+ reference "internal";
+ }
+
+ /*
+ * Features
+ */
+
+ /*
+ * Reachable from Linux on the PCP paths only. The DCB APP PCP
+ * selector indexes 16 values, PCP plus DEI, so dcb app pcp-prio keys
+ * on both; on egress dcb rewr prio-pcp takes the nd/de suffix. sparx5
+ * offloads both directions in mainline. The DSCP paths carry no
+ * second dimension.
+ */
+ feature drop-eligible {
+ description "Platform can carry a single drop-eligible bit alongside
+ the priority.
+
+ This is the drop_eligible parameter of IEEE 802.1Q-2022 6.9.3.";
+ reference "6.9.3 of IEEE Std 802.1Q-2022";
+ }
+
+ /*
+ * No Linux interface assigns more than the one bit. Platforms with
+ * more levels program them through a platform-specific path.
+ */
+ feature drop-precedence {
+ description "Platform can carry more than two levels of drop
+ precedence through classification, policing and queue admission.
+
+ Supersedes drop-eligible. A platform advertising this need not
+ advertise drop-eligible as well; one advertising only drop-eligible
+ accepts values 0 and 1.";
+ }
+
+ /*
+ * Typedefs and groupings
+ */
+
+ /*
+ * Enabled where tc tbf offloads to the port shaper, or where the CPU is
+ * the forwarding path and software tbf is the real thing.
+ */
+ feature rate-limit {
+ description "Platform supports a port-level egress rate cap.";
+ }
+
+ typedef trust-order-type {
+ type enumeration {
+ enum none {
+ description "Trust nothing, every frame gets the defaults.";
+ }
+ enum pcp {
+ description "Believe the PCP of VLAN-tagged frames.";
+ }
+ enum dscp {
+ description "Believe the DSCP of IP frames.";
+ }
+ enum dscp-pcp {
+ description "DSCP of IP frames first, then PCP of tagged frames.";
+ }
+ enum pcp-dscp {
+ description "PCP of tagged frames first, then DSCP of IP frames.";
+ }
+ }
+ description "Trusted classification fields in order of precedence.";
+ }
+
+ typedef drop-precedence-type {
+ type uint8 {
+ range "0..3";
+ }
+ description "Drop precedence assigned during ingress classification.
+
+ 0 is least likely to be discarded under congestion. IEEE
+ 802.1Q-2022 models a single drop_eligible bit (6.9.3); this is its
+ generalisation. Platforms with two levels use 0 and 1. Read
+ max-drop-precedence-levels for the depth implemented; values at or
+ above it are rejected.";
+ reference "6.9.3 of IEEE Std 802.1Q-2022";
+ }
+
+ grouping classified-result {
+ description "What ingress classification yields for one frame.";
+
+ leaf priority {
+ type dot1q-types:priority-type;
+ mandatory true;
+ description "Internal priority, 0 to 7.";
+ reference "6.9.3 of IEEE Std 802.1Q-2022";
+ }
+
+ leaf drop-precedence {
+ if-feature "drop-eligible or drop-precedence";
+ type drop-precedence-type;
+ description "Drop precedence.
+
+ Platforms with fewer levels than the value given reject it; see
+ max-drop-precedence-levels.";
+ }
+ }
+
+ /*
+ * Data Nodes
+ */
+
+ augment "/if:interfaces/if:interface" {
+ description "Per-interface QoS.";
+
+ container qos {
+ description "Ingress classification and egress transmission selection.";
+
+ container capabilities {
+ config false;
+ description "What the platform implements.
+
+ Read these before writing configuration that depends on table
+ depth.";
+
+ leaf max-drop-precedence-levels {
+ if-feature "drop-eligible or drop-precedence";
+ type uint8 {
+ range "1..4";
+ }
+ description "Number of drop-precedence levels.
+
+ Two means only 0 and 1 may be used, which is what a platform
+ advertising drop-eligible alone reports.";
+ }
+
+ leaf max-traffic-classes {
+ type dot1q-types:num-traffic-class-type;
+ default 8;
+ description "Number of egress traffic classes.
+
+ The port's transmit queue count when that is between two
+ and seven. A port with a single queue has no queue
+ structure to respect and gets the kernel's eight classes.";
+ reference "12.6.3.1 of IEEE Std 802.1Q-2022";
+ }
+
+ leaf-list offload {
+ type enumeration {
+ enum classification {
+ description "Ingress classification and trust order run
+ in the switch fabric.";
+ }
+ enum remarking {
+ description "Egress remarking runs in the switch fabric.";
+ }
+ enum transmission-selection {
+ description "Traffic classes and transmission selection
+ run in the switch fabric.";
+ }
+ enum rate-limit {
+ description "The port rate limit runs in the switch fabric.";
+ }
+ }
+ description "Pipeline stages the driver has taken into
+ hardware. A stage not listed runs in the kernel, which on
+ a switch covers only CPU-forwarded frames.";
+ }
+
+ /* Read from the driver's dcb apptrust policy set. */
+ leaf-list supported-trust-order {
+ type trust-order-type;
+ description "Trust orders the platform accepts; none is always
+ accepted.
+
+ An order that is not listed is rejected rather than
+ reordered.";
+ }
+ }
+
+ container ingress {
+ description "Classification.";
+
+ /* Applied as dcb apptrust, which has the same ordering semantics. */
+ leaf trust {
+ type trust-order-type;
+ default "pcp";
+ description "Which header fields are believed, and in what order.
+
+ The first that yields a result for a frame wins. The
+ default is pcp, the 802.1Q bridge behaviour: the tag is
+ trusted and untagged frames get the port defaults. Edge
+ ports that classify on behalf of the network set dscp or
+ dscp-pcp. none turns classification off, assigning
+ default-priority and default-drop-precedence to every
+ frame. Platforms constrain the available orders; see
+ supported-trust-order.";
+ }
+
+ /* Applied as dcb app default-prio. */
+ leaf default-priority {
+ type dot1q-types:priority-type;
+ default 0;
+ description "Priority assigned to frames not matched by any
+ trusted classifier.";
+ reference "12.6.2 of IEEE Std 802.1Q-2022";
+ }
+
+ leaf default-drop-precedence {
+ if-feature "drop-eligible or drop-precedence";
+ type drop-precedence-type;
+ default 0;
+ description "Drop precedence assigned to frames not matched by
+ any trusted classifier.";
+ }
+
+ /*
+ * Applied as dcb app pcp-prio, which carries the priority
+ * only; drop precedence needs a platform-specific path.
+ */
+ container pcp-map {
+ description "PCP and DEI of the outermost VLAN tag to a
+ (priority, drop precedence) pair.";
+ reference "6.9.3 of IEEE Std 802.1Q-2022";
+
+ choice mode {
+ default "preset";
+ description "A named preset, or an explicit table.";
+
+ case preset {
+ leaf preset {
+ type enumeration {
+ enum ieee {
+ description "The 8P0D default of IEEE 802.1Q-2022
+ Table 6-3: PCP n maps to priority n.";
+ }
+ }
+ default "ieee";
+ description "Named PCP mapping.";
+ reference "6.9.3, Table 6-3 of IEEE Std 802.1Q-2022";
+ }
+ }
+
+ case custom {
+ list entry {
+ key "pcp dei";
+ description "Explicit per-(PCP, DEI) mapping.
+
+ Omitted combinations fall back to default-priority and
+ default-drop-precedence.";
+
+ leaf pcp {
+ type dot1q-types:priority-type;
+ description "PCP value in the tag.";
+ }
+
+ leaf dei {
+ type boolean;
+ description "DEI bit in the tag.";
+ }
+
+ uses classified-result;
+ }
+ }
+ }
+ }
+
+ /* Applied as dcb app dscp-prio. */
+ container dscp-map {
+ description "DSCP to a (priority, drop precedence) pair.";
+
+ choice mode {
+ default "preset";
+ description "A named preset, or an explicit table.";
+
+ case preset {
+ leaf preset {
+ type enumeration {
+ enum msb {
+ description "Top three bits of the DSCP field select
+ priority 0 to 7.";
+ }
+ enum ietf {
+ description "RFC 4594 per-hop-behaviour groups.
+
+ Unmapped codepoints fall back to
+ default-priority.";
+ }
+ }
+ default "ietf";
+ description "Named DSCP mapping.";
+ reference "RFC 4594";
+ }
+ }
+
+ case custom {
+ list entry {
+ key "dscp";
+ description "Explicit per-codepoint mapping.";
+
+ leaf dscp {
+ type inet:dscp;
+ description "Codepoint in the IP header.";
+ }
+
+ leaf trusted {
+ type boolean;
+ default true;
+ description "Whether this codepoint is trusted.
+
+ When false the codepoint is ignored and the frame
+ falls through to the next trusted field, or to
+ default-priority.";
+ }
+
+ uses classified-result;
+ }
+ }
+ }
+ }
+ }
+
+ container egress {
+ description "Traffic-class binding, selection, and remarking.";
+
+ /*
+ * Values at or above the active class count are rejected by
+ * the daemon.
+ */
+ container traffic-class-table {
+ description "Priority to traffic-class binding.
+
+ Node names follow the IEEE 802.1Q-2022 traffic class
+ table.";
+ reference "12.6.3, 8.6.6 of IEEE Std 802.1Q-2022";
+
+ choice mode {
+ default "preset";
+ description "A named preset, or an explicit table.";
+
+ case preset {
+ leaf preset {
+ type enumeration {
+ enum ieee {
+ description "The ordinary bridge mapping of IEEE
+ 802.1Q-2022 Table 8-5.
+
+ The table column for the port's traffic class
+ count, capabilities/max-traffic-classes.";
+ }
+ enum ieee-sr {
+ description "The TSN mapping of IEEE 802.1Q-2022
+ Table 34-1: SR classes A and B on the two
+ highest traffic classes.
+
+ The map only; transmission selection algorithms
+ are set per traffic class.";
+ }
+ }
+ default "ieee";
+ description "Named priority to traffic-class mapping.";
+ reference "Table 8-5, Table 34-1 of IEEE Std
+ 802.1Q-2022";
+ }
+ }
+
+ case custom {
+ /*
+ * A priority left unset falls back to the ieee preset's
+ * value for it.
+ */
+ leaf priority0 { type dot1q-types:traffic-class-type; }
+ leaf priority1 { type dot1q-types:traffic-class-type; }
+ leaf priority2 { type dot1q-types:traffic-class-type; }
+ leaf priority3 { type dot1q-types:traffic-class-type; }
+ leaf priority4 { type dot1q-types:traffic-class-type; }
+ leaf priority5 { type dot1q-types:traffic-class-type; }
+ leaf priority6 { type dot1q-types:traffic-class-type; }
+ leaf priority7 { type dot1q-types:traffic-class-type; }
+ }
+ }
+ }
+
+ list traffic-class {
+ key "id";
+ max-elements 8;
+ description "Per-traffic-class transmission selection.
+
+ A class with no entry runs strict-priority.";
+
+ /* Ids at or above capabilities/max-traffic-classes are
+ * rejected by the daemon.
+ */
+ leaf id {
+ type dot1q-types:traffic-class-type;
+ description "Traffic class index.";
+ }
+
+ leaf algorithm {
+ type identityref {
+ base dot1q-types:transmission-selection-algorithm;
+ }
+ default "dot1q-types:strict-priority";
+ description "Transmission selection algorithm.
+
+ Selecting credit-based-shaper turns the shaper on; its
+ parameters live in ieee802-dot1dc-cbsa-if.";
+ reference "12.20.2, 8.6.8 and Table 8-6 of IEEE Std
+ 802.1Q-2022";
+ }
+
+ /* Applied as a tc ets quantum: the share times the MTU. */
+ leaf bandwidth {
+ when "derived-from-or-self(../algorithm, 'dot1q-types:"
+ + "enhanced-transmission-selection')";
+ type uint8 {
+ range "1..100";
+ }
+ units "percent";
+ must "sum(../../traffic-class[derived-from-or-self("
+ + "algorithm, 'dot1q-types:enhanced-transmission-"
+ + "selection')]/bandwidth) = 100" {
+ error-message "The ETS shares on a port must sum to 100.";
+ }
+ mandatory true;
+ description "TCBandwidth: the percentage of the bandwidth
+ left by the strict and CBS classes that this class is
+ assigned. The ETS classes on a port sum to 100.";
+ reference "37.2, 37.3 of IEEE Std 802.1Q-2022";
+ }
+ }
+
+ /* Applied as dcb rewr, whose prio-pcp values carry the DEI bit. */
+ container remark {
+ description "Rewrite PCP, DEI, and DSCP on transmit from the
+ internal (priority, drop precedence) pair.";
+ reference "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022";
+
+ leaf pcp {
+ type enumeration {
+ enum none {
+ description "No rewrite is configured. The PCP a tagged
+ frame leaves with is what the forwarding path gives
+ it: the received PCP where frames pass the kernel,
+ the frame's priority on a switch fabric that encodes
+ it as 6.9.3 of IEEE Std 802.1Q-2022 specifies.";
+ }
+ enum from-priority {
+ description "Derive PCP and DEI from the classification
+ result.";
+ }
+ }
+ default "none";
+ description "PCP and DEI rewrite on transmit.";
+ }
+
+ leaf dscp {
+ type enumeration {
+ enum none {
+ description "Leave DSCP untouched.";
+ }
+ enum from-priority {
+ description "Derive DSCP from the classification
+ result.";
+ }
+ }
+ default "none";
+ description "DSCP rewrite on transmit.";
+ }
+ }
+
+ /* Applied as a tc tbf root above the scheduler. */
+ container rate-limit {
+ if-feature "rate-limit";
+ presence "Bounds the transmit rate of the port.";
+ description "One token bucket on the whole port, above the
+ traffic classes. It bounds what the port sends in total
+ and knows nothing about classes: transmission selection
+ decides which class gets the bounded rate.";
+
+ leaf rate {
+ type uint64 {
+ range "8..max";
+ }
+ units "bits/second";
+ mandatory true;
+ description "Sustained transmit rate.";
+ }
+
+ leaf burst {
+ type uint32 {
+ range "1518..max";
+ }
+ units "bytes";
+ description "Bytes that may leave at once when the bucket is
+ full. Unset, the daemon sizes it for ten milliseconds
+ at rate, never below one maximum-size frame.";
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/confd/yang/confd/infix-if-qos@2026-09-07.yang b/src/confd/yang/confd/infix-if-qos@2026-09-07.yang
new file mode 120000
index 0000000000..591c3dc64b
--- /dev/null
+++ b/src/confd/yang/confd/infix-if-qos@2026-09-07.yang
@@ -0,0 +1 @@
+infix-if-qos.yang
\ No newline at end of file
diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang
index 5182ec8eb4..43f9eaf17b 100644
--- a/src/confd/yang/confd/infix-interfaces.yang
+++ b/src/confd/yang/confd/infix-interfaces.yang
@@ -38,6 +38,7 @@ module infix-interfaces {
include infix-if-ppp;
include infix-if-ptp;
include infix-if-modem;
+ include infix-if-qos;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
@@ -64,6 +65,13 @@ module infix-interfaces {
reference "internal";
}
+ revision 2026-09-07 {
+ description "Add per-interface QoS: ingress classification, traffic
+ class table and transmission selection, see
+ infix-if-qos@2026-09-07.";
+ reference "internal";
+ }
+
revision 2026-06-11 {
description "Fix WireGuard key-format must expressions, see
infix-if-wireguard@2026-06-11.";
diff --git a/src/yangerd/README.md b/src/yangerd/README.md
index 794088b159..4dcacd940b 100644
--- a/src/yangerd/README.md
+++ b/src/yangerd/README.md
@@ -63,10 +63,11 @@ yangerd deletes the key rather than storing an empty container, because
libyang would instantiate a presence container from `{}`. statd treats
an empty answer as "nothing to add", not as an error.
-**Long-lived helpers instead of forks.** `ip`, `ip -s -d` and `bridge`
-run as `-json -force -batch -` subprocesses (`internal/ipbatch`), fed one
-command per event. chronyd is queried over cmdmon, ptp4l over its
-management socket, FRR over the daemons' vty sockets, all in-process.
+**Long-lived helpers instead of forks.** `ip`, `ip -s -d`, `bridge`
+and `tc` run as `-json -force -batch -` subprocesses
+(`internal/ipbatch`), fed one command per event. chronyd is queried over cmdmon, ptp4l over its
+management socket, FRR over the daemons' vty sockets, the DCB tables
+over rtnetlink, all in-process.
**Restart, don't die.** Every monitor runs under `backoff.Retry`: when
its source goes away (FRR restart, dnsmasq exit, `lldpcli watch` ending),
@@ -89,6 +90,7 @@ build vendors them from `go.sum`.
|---|---|---|---|
| `ietf-interfaces:interfaces` | links, addresses, neighbours, bridge MDB, VLANs, LAG | event | rtnetlink, re-read with `ip`/`bridge` batch |
| | Ethernet speed, duplex, PMD | event | ethtool genetlink monitor, sweep at start |
+| | QoS capabilities | event | rtnetlink TC and DCB, re-read with `tc` batch and DCB over rtnetlink |
| | Wi-Fi station, AP, mesh point | event | nl80211 and wpa_supplicant/hostapd control sockets |
| | STP port and bridge state | poll 5 s | mstpd |
| | WireGuard peers | poll 10 s | `wg` netlink |
diff --git a/src/yangerd/cmd/yangerd/main.go b/src/yangerd/cmd/yangerd/main.go
index 9a6e74b672..44e8b14875 100644
--- a/src/yangerd/cmd/yangerd/main.go
+++ b/src/yangerd/cmd/yangerd/main.go
@@ -34,6 +34,7 @@ import (
"github.com/kernelkit/infix/src/yangerd/internal/mmmonitor"
"github.com/kernelkit/infix/src/yangerd/internal/monitor"
"github.com/kernelkit/infix/src/yangerd/internal/ptpmonitor"
+ "github.com/kernelkit/infix/src/yangerd/internal/qosmonitor"
"github.com/kernelkit/infix/src/yangerd/internal/sysreaders"
"github.com/kernelkit/infix/src/yangerd/internal/tftpmonitor"
"github.com/kernelkit/infix/src/yangerd/internal/tree"
@@ -185,6 +186,19 @@ func main() {
return qs
})
+ if qosMon := startQoS(ctx, nlmon.Links, slogLog); qosMon != nil {
+ qosMon.SetOnUpdate(nlmon.SetQoSData)
+ nlmon.SetLinkRefresh(qosMon.Refresh)
+ spawn("qosmonitor", func(ctx context.Context) error {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-nlmon.WaitReady():
+ }
+ return qosMon.Run(ctx)
+ })
+ }
+
// inotify says nothing when a /proc/sys/net/*/conf/ directory
// comes or goes, so follow the interface set from netlink instead.
linkSetCh := make(chan struct{}, 1)
@@ -504,3 +518,20 @@ func makeBootOrderReader(t *tree.Tree, cmd collector.CommandRunner) func(string)
return json.Marshal(map[string]interface{}{"infix-system:software": sw})
}
}
+
+// startQoS starts the QoS monitor, its tc batch and DCB reader. Without
+// them the ports carry no QoS state, which is no reason to stop.
+func startQoS(ctx context.Context, links func() json.RawMessage, log *slog.Logger) *qosmonitor.Monitor {
+ tc, err := ipbatch.NewTC(ctx, log)
+ if err != nil {
+ log.Warn("no QoS state, tc batch did not start", "err", err)
+ return nil
+ }
+ dcb, err := qosmonitor.DialDCB()
+ if err != nil {
+ log.Warn("no QoS state, DCB socket did not open", "err", err)
+ tc.Close()
+ return nil
+ }
+ return qosmonitor.New(tc, dcb, links, log)
+}
diff --git a/src/yangerd/internal/ipbatch/ipbatch.go b/src/yangerd/internal/ipbatch/ipbatch.go
index 9fe21517f8..6865ccae8d 100644
--- a/src/yangerd/internal/ipbatch/ipbatch.go
+++ b/src/yangerd/internal/ipbatch/ipbatch.go
@@ -45,6 +45,8 @@ var ErrBatchDead = errors.New("batch process is dead")
// command, typically because the device it names does not exist.
var ErrCommandFailed = errors.New("batch command failed")
+var errTimeout = errors.New("timeout waiting for response")
+
const (
queryTimeout = 5 * time.Second
@@ -104,9 +106,14 @@ func NewBridge(ctx context.Context, log *slog.Logger) (*Batch, error) {
return Start(ctx, log, []string{"bridge", "-json", "-d", "-force", "-batch", "-"}, "vlan show dev lo")
}
+// NewTC starts `tc -json -force -batch -`.
+func NewTC(ctx context.Context, log *slog.Logger) (*Batch, error) {
+ return Start(ctx, log, []string{"tc", "-json", "-force", "-batch", "-"}, "qdisc show dev lo")
+}
+
// Start runs argv as a batch subprocess and keeps it running, restarting
// it with backoff when it dies. canary is a command that must succeed,
-// used to validate a restarted subprocess.
+// used to validate the first subprocess and every restarted one.
func Start(ctx context.Context, log *slog.Logger, argv []string, canary string) (*Batch, error) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
@@ -117,8 +124,10 @@ func Start(ctx context.Context, log *slog.Logger, argv []string, canary string)
cancel: cancel,
died: make(chan struct{}, 1),
}
- if err := b.start(); err != nil {
- cancel()
+ // A subprocess that rejects the canary or exits will not do better
+ // on a restart, one that is only slow to answer may.
+ if err := b.Refresh(); err != nil && !errors.Is(err, errTimeout) {
+ b.Close()
return nil, err
}
go b.restartLoop()
@@ -161,13 +170,25 @@ func (b *Batch) start() error {
b.alive.Store(true)
b.mu.Unlock()
- go b.readLines(r, lines, quit, gen)
+ go b.readLines(r, lines, quit)
+ go b.wait(cmd, gen)
return nil
}
+// wait reaps one subprocess and marks it dead if it is still current.
+// Its output may still sit unread in lines, so exit is not left to the
+// reader to notice.
+func (b *Batch) wait(cmd *exec.Cmd, gen int64) {
+ err := cmd.Wait()
+ if b.gen.Load() == gen {
+ b.log.Debug(b.argv[0]+" batch: subprocess exited", "err", err)
+ b.markDead()
+ }
+}
+
// readLines feeds the merged stdout and stderr of one subprocess to
-// lines until it exits or is replaced.
-func (b *Batch) readLines(r io.ReadCloser, lines chan<- []byte, quit <-chan struct{}, gen int64) {
+// lines until it exits, is replaced or the Batch is closed.
+func (b *Batch) readLines(r io.ReadCloser, lines chan<- []byte, quit <-chan struct{}) {
defer r.Close()
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
@@ -179,9 +200,6 @@ func (b *Batch) readLines(r io.ReadCloser, lines chan<- []byte, quit <-chan stru
}
}
close(lines)
- if b.gen.Load() == gen {
- b.markDead()
- }
}
func (b *Batch) markDead() {
@@ -246,7 +264,7 @@ func (b *Batch) Query(command string) (json.RawMessage, error) {
if b.cmd.Process != nil {
b.cmd.Process.Kill()
}
- return nil, fmt.Errorf("timeout waiting for response to: %s", command)
+ return nil, fmt.Errorf("%w to: %s", errTimeout, command)
}
}
}
@@ -280,7 +298,6 @@ func reap(cmd *exec.Cmd, stdin io.Closer) {
}
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
- go cmd.Wait()
}
}
@@ -300,6 +317,10 @@ func (b *Batch) Close() {
if b.cmd != nil && b.cmd.Process != nil {
b.cmd.Process.Kill()
}
+ if b.quit != nil {
+ close(b.quit)
+ b.quit = nil
+ }
b.alive.Store(false)
}
diff --git a/src/yangerd/internal/ipbatch/ipbatch_test.go b/src/yangerd/internal/ipbatch/ipbatch_test.go
index 6e6a50c16b..c9b5525458 100644
--- a/src/yangerd/internal/ipbatch/ipbatch_test.go
+++ b/src/yangerd/internal/ipbatch/ipbatch_test.go
@@ -12,7 +12,8 @@ import (
// fakeBatch behaves like `ip -force -batch -`: one JSON line per good
// command, "Command failed -:N" on stderr for a bad one (after a stray
-// "[]" for "junk"), a stall for "hang", and exit for "die".
+// "[]" for "junk"), a stall for "hang", exit for "die", and for "later"
+// an answer, then unasked output and exit.
const fakeBatch = `#!/bin/sh
n=0
while read -r cmd; do
@@ -23,18 +24,25 @@ while read -r cmd; do
junk*) echo "[]"; echo "Cannot find device" >&2; echo "Command failed -:$n" >&2 ;;
hang*) sleep 30 ;;
die*) exit 1 ;;
+ later*) (sleep 0.2; i=0; while [ $i -lt 20 ]; do echo noise; i=$((i+1)); done; kill $$) &
+ echo "[\"$cmd\"]" ;;
*) echo "[\"$cmd\"]" ;;
esac
done
`
-func startFake(t *testing.T) *Batch {
+func fakeScript(t *testing.T) string {
t.Helper()
script := filepath.Join(t.TempDir(), "fake")
if err := os.WriteFile(script, []byte(fakeBatch), 0755); err != nil {
t.Fatal(err)
}
- b, err := Start(context.Background(), slog.Default(), []string{script}, "canary")
+ return script
+}
+
+func startFake(t *testing.T) *Batch {
+ t.Helper()
+ b, err := Start(context.Background(), slog.Default(), []string{fakeScript(t)}, "canary")
if err != nil {
t.Fatal(err)
}
@@ -42,6 +50,17 @@ func startFake(t *testing.T) *Batch {
return b
}
+// A subprocess that rejects the canary, or exits at once, as when it
+// is given options it does not take, fails Start.
+func TestStartChecksCanary(t *testing.T) {
+ if _, err := Start(context.Background(), slog.Default(), []string{fakeScript(t)}, "fail"); !errors.Is(err, ErrCommandFailed) {
+ t.Fatalf("rejected canary: err = %v, want ErrCommandFailed", err)
+ }
+ if _, err := Start(context.Background(), slog.Default(), []string{"false"}, "canary"); err == nil {
+ t.Fatal("exited subprocess: Start succeeded")
+ }
+}
+
func TestQueryAnswers(t *testing.T) {
b := startFake(t)
got, err := b.Query("link show dev eth0")
@@ -90,6 +109,22 @@ func TestQueryDeadThenRestart(t *testing.T) {
t.Fatal("subprocess was not restarted")
}
+// A subprocess that exits between queries is noticed, even with more
+// output than the reader can queue left behind.
+func TestExitBetweenQueries(t *testing.T) {
+ b := startFake(t)
+ if _, err := b.Query("later"); err != nil {
+ t.Fatal(err)
+ }
+ deadline := time.Now().Add(2 * time.Second)
+ for b.Alive() {
+ if time.Now().After(deadline) {
+ t.Fatal("exit not noticed")
+ }
+ time.Sleep(5 * time.Millisecond)
+ }
+}
+
// A command that gets no answer at all times out and kills the process.
func TestQueryTimeout(t *testing.T) {
if testing.Short() {
diff --git a/src/yangerd/internal/monitor/monitor.go b/src/yangerd/internal/monitor/monitor.go
index ee8326e943..d9b2233a65 100644
--- a/src/yangerd/internal/monitor/monitor.go
+++ b/src/yangerd/internal/monitor/monitor.go
@@ -46,6 +46,7 @@ type NLMonitor struct {
brBatch *ipbatch.Batch
tree *tree.Tree
ethRefresh func(string)
+ onLink func(string)
linkSet func()
queriers func() []mcd.Querier
wwan func() map[string]any
@@ -73,6 +74,7 @@ type NLMonitor struct {
wifi map[string]json.RawMessage // ifname → wifi JSON
wireguard map[string]json.RawMessage // ifname → WireGuard peer-status JSON
lastWG string
+ qos map[string]json.RawMessage // ifname → QoS members of the entry
// vlans is `bridge vlan show` by interface, vlanGlobals the
// per-bridge `bridge vlan global show`. Both are replaced whole
@@ -146,6 +148,7 @@ func New(linkBatch, addrBatch, neighBatch, brBatch *ipbatch.Batch, t *tree.Tree,
ethernet: make(map[string]json.RawMessage),
wifi: make(map[string]json.RawMessage),
wireguard: make(map[string]json.RawMessage),
+ qos: make(map[string]json.RawMessage),
containers: make(map[string]json.RawMessage),
lastOperStatus: make(map[string]string),
lastChange: make(map[string]time.Time),
@@ -158,6 +161,18 @@ func (m *NLMonitor) SetEthRefresh(fn func(string)) {
m.ethRefresh = fn
}
+// SetLinkRefresh sets an optional callback run with the name of any
+// interface that changed, appeared or went away.
+func (m *NLMonitor) SetLinkRefresh(fn func(string)) {
+ m.onLink = fn
+}
+
+func (m *NLMonitor) linkRefreshed(name string) {
+ if m.onLink != nil && name != "" {
+ m.onLink(name)
+ }
+}
+
// SetLinkSetChange sets an optional callback run when an interface
// appears, goes away or is renamed.
func (m *NLMonitor) SetLinkSetChange(fn func()) {
@@ -205,6 +220,21 @@ func (m *NLMonitor) SetWifiData(ifname string, data json.RawMessage) {
m.rebuild()
}
+// SetQoSData updates the QoS members of interface entries, nil drops
+// them, and rebuilds the YANG document once for all of them.
+func (m *NLMonitor) SetQoSData(changed map[string]json.RawMessage) {
+ m.mu.Lock()
+ for ifname, data := range changed {
+ if data == nil {
+ delete(m.qos, ifname)
+ } else {
+ m.qos[ifname] = data
+ }
+ }
+ m.mu.Unlock()
+ m.rebuild()
+}
+
// SetContainerInterfaces replaces the interfaces found in container
// network namespaces, by configured name. The document is rebuilt only
// when something changed.
@@ -478,8 +508,9 @@ func (m *NLMonitor) handleLinkUpdate(update netlink.LinkUpdate) bool {
wasBridge := bridgeRelated(m.links, index)
m.mu.Unlock()
- if update.Header.Type == syscall.RTM_DELLINK {
+ if linkGone(update) {
defer m.linkSetChanged()
+ defer m.linkRefreshed(old)
changed := m.removeInterface(index)
if wasBridge {
m.refreshBridgeVLANs()
@@ -738,7 +769,9 @@ func (m *NLMonitor) refreshInterface(index int, name string) bool {
if old != name {
m.linkSetChanged()
+ m.linkRefreshed(old)
}
+ m.linkRefreshed(name)
if m.ethRefresh != nil && iface.IsEthernet(linkRaw, m.fc) {
m.ethRefresh(name)
}
@@ -761,6 +794,7 @@ func (m *NLMonitor) rebuild() {
mdb: copyStringMap(m.mdb),
rtrPorts: maps.Clone(m.rtrPorts),
wireguard: copyStringMap(m.wireguard),
+ qos: copyStringMap(m.qos),
vlans: maps.Clone(m.vlans),
vlanGlobals: maps.Clone(m.vlanGlobals),
}
@@ -824,7 +858,7 @@ func writeSortedRaw(b *strings.Builder, prefix string, m map[string]json.RawMess
// augments is everything merged into the document iface.Transform
// produced, keyed by interface name unless noted.
type augments struct {
- ethernet, wifi, mdb, bridgeSTP, portSTP, wireguard map[string]json.RawMessage
+ ethernet, wifi, mdb, bridgeSTP, portSTP, wireguard, qos map[string]json.RawMessage
rtrPorts map[string][]string // bridge multicast router ports, by bridge
bridges map[string]iface.BridgeInfo // by bridge name, from the link dump
@@ -834,7 +868,8 @@ type augments struct {
func (a *augments) empty() bool {
return len(a.ethernet) == 0 && len(a.wifi) == 0 && len(a.mdb) == 0 &&
- len(a.bridgeSTP) == 0 && len(a.portSTP) == 0 && len(a.wireguard) == 0 && len(a.bridges) == 0
+ len(a.bridgeSTP) == 0 && len(a.portSTP) == 0 && len(a.wireguard) == 0 && len(a.qos) == 0 &&
+ len(a.bridges) == 0
}
// mergeAugments adds ethernet, wifi, and bridge data into the
@@ -968,6 +1003,13 @@ func mergeAugments(doc json.RawMessage, a augments) json.RawMessage {
}
}
+ if qosData, ok := a.qos[name]; ok {
+ var members map[string]any
+ if err := json.Unmarshal(qosData, &members); err == nil {
+ maps.Copy(ifaceObj, members)
+ }
+ }
+
ifaceArr[i] = ifaceObj
}
@@ -1331,6 +1373,12 @@ func ifNameByIndex(index int) (string, error) {
return iface.Name, nil
}
+// linkGone tells whether a link update removes the interface. The
+// bridge sends a delete of family AF_BRIDGE when a port leaves it.
+func linkGone(update netlink.LinkUpdate) bool {
+ return update.Header.Type == syscall.RTM_DELLINK && update.Family != syscall.AF_BRIDGE
+}
+
func linkNameFromUpdate(update netlink.LinkUpdate) (string, bool) {
if update.Link != nil && update.Link.Attrs() != nil && update.Link.Attrs().Name != "" {
return update.Link.Attrs().Name, true
diff --git a/src/yangerd/internal/monitor/monitor_test.go b/src/yangerd/internal/monitor/monitor_test.go
index 7228114273..93d57715b3 100644
--- a/src/yangerd/internal/monitor/monitor_test.go
+++ b/src/yangerd/internal/monitor/monitor_test.go
@@ -345,6 +345,25 @@ func TestLateDeleteKeepsReusedName(t *testing.T) {
}
}
+func TestLinkGone(t *testing.T) {
+ for _, tc := range []struct {
+ typ uint16
+ family uint8
+ want bool
+ }{
+ {syscall.RTM_DELLINK, syscall.AF_UNSPEC, true},
+ {syscall.RTM_DELLINK, syscall.AF_BRIDGE, false},
+ {syscall.RTM_NEWLINK, syscall.AF_UNSPEC, false},
+ } {
+ var u netlink.LinkUpdate
+ u.Header.Type = tc.typ
+ u.Family = tc.family
+ if got := linkGone(u); got != tc.want {
+ t.Errorf("linkGone(type %d, family %d) = %v, want %v", tc.typ, tc.family, got, tc.want)
+ }
+ }
+}
+
func TestLinkRowFor(t *testing.T) {
for _, tc := range []struct {
raw string
diff --git a/src/yangerd/internal/qosmonitor/dcb.go b/src/yangerd/internal/qosmonitor/dcb.go
new file mode 100644
index 0000000000..29ddb4c77f
--- /dev/null
+++ b/src/yangerd/internal/qosmonitor/dcb.go
@@ -0,0 +1,100 @@
+package qosmonitor
+
+import (
+ "github.com/mdlayher/netlink"
+ "golang.org/x/sys/unix"
+)
+
+// Attributes of an IEEE DCB reply, see linux/dcbnl.h.
+const (
+ dcbCmdIEEEGet = 21
+ dcbAttrIEEE = 13
+ dcbAttrAppTable = 3
+ dcbAttrRewrTable = 12
+ dcbSelectorDSCP = 5
+ dcbSelectorPCP = 255
+)
+
+// DCB reads the DCB application and rewrite tables of a port over
+// rtnetlink. dcb(8) cannot serve as a batch helper for this: it writes
+// one JSON document for the life of the process. Only the monitor's
+// worker calls it.
+type DCB struct {
+ conn *netlink.Conn
+}
+
+// DialDCB opens the rtnetlink socket the tables are read from.
+func DialDCB() (*DCB, error) {
+ conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &DCB{conn: conn}, nil
+}
+
+// Tables reports whether the port maps PCP or DSCP to priority in its
+// application table, and priority to PCP or DSCP in its rewrite table.
+func (d *DCB) Tables(ifname string) (app, rewr bool, err error) {
+ ae := netlink.NewAttributeEncoder()
+ ae.String(dcbAttrIfname, ifname)
+ attrs, err := ae.Encode()
+ if err != nil {
+ return false, false, err
+ }
+ req := netlink.Message{
+ Header: netlink.Header{Type: unix.RTM_GETDCB, Flags: netlink.Request},
+ Data: append([]byte{unix.AF_UNSPEC, dcbCmdIEEEGet, 0, 0}, attrs...),
+ }
+
+ msgs, err := d.conn.Execute(req)
+ if err != nil {
+ return false, false, err
+ }
+ for _, msg := range msgs {
+ a, r := parseIEEE(msg.Data)
+ app, rewr = app || a, rewr || r
+ }
+ return app, rewr, nil
+}
+
+func parseIEEE(data []byte) (app, rewr bool) {
+ if len(data) < sizeofDcbmsg {
+ return false, false
+ }
+ ad, err := netlink.NewAttributeDecoder(data[sizeofDcbmsg:])
+ if err != nil {
+ return false, false
+ }
+ for ad.Next() {
+ if ad.Type() != dcbAttrIEEE {
+ continue
+ }
+ ad.Nested(func(nad *netlink.AttributeDecoder) error {
+ for nad.Next() {
+ switch nad.Type() {
+ case dcbAttrAppTable:
+ app = hasPrioMap(nad.Bytes())
+ case dcbAttrRewrTable:
+ rewr = hasPrioMap(nad.Bytes())
+ }
+ }
+ return nil
+ })
+ }
+ return app, rewr
+}
+
+// hasPrioMap tells whether a table of struct dcb_app holds a PCP or DSCP
+// entry. Each entry leads with its selector.
+func hasPrioMap(table []byte) bool {
+ ad, err := netlink.NewAttributeDecoder(table)
+ if err != nil {
+ return false
+ }
+ for ad.Next() {
+ if b := ad.Bytes(); len(b) > 0 && (b[0] == dcbSelectorDSCP || b[0] == dcbSelectorPCP) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/src/yangerd/internal/qosmonitor/dcb_test.go b/src/yangerd/internal/qosmonitor/dcb_test.go
new file mode 100644
index 0000000000..bf87749ce7
--- /dev/null
+++ b/src/yangerd/internal/qosmonitor/dcb_test.go
@@ -0,0 +1,58 @@
+package qosmonitor
+
+import (
+ "testing"
+
+ "github.com/mdlayher/netlink"
+)
+
+// ieeeReply is an RTM_GETDCB reply carrying app and rewrite tables of
+// struct dcb_app: selector, priority, protocol.
+func ieeeReply(t *testing.T, app, rewr [][]byte) []byte {
+ t.Helper()
+ table := func(entries [][]byte) func(*netlink.AttributeEncoder) error {
+ return func(ae *netlink.AttributeEncoder) error {
+ for _, e := range entries {
+ ae.Bytes(1, e)
+ }
+ return nil
+ }
+ }
+ ae := netlink.NewAttributeEncoder()
+ ae.String(dcbAttrIfname, "e1")
+ ae.Nested(dcbAttrIEEE, func(nae *netlink.AttributeEncoder) error {
+ nae.Bytes(1, make([]byte, 59)) // ETS, skipped
+ nae.Nested(dcbAttrAppTable, table(app))
+ nae.Nested(dcbAttrRewrTable, table(rewr))
+ return nil
+ })
+ attrs, err := ae.Encode()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return append([]byte{0, dcbCmdIEEEGet, 0, 0}, attrs...)
+}
+
+func TestParseIEEE(t *testing.T) {
+ dscp := []byte{dcbSelectorDSCP, 5, 46, 0}
+ pcp := []byte{dcbSelectorPCP, 3, 3, 0}
+ ethertype := []byte{1, 3, 0x8e, 0x88}
+
+ for _, tc := range []struct {
+ name string
+ app, rewr [][]byte
+ wantApp bool
+ wantRewr bool
+ }{
+ {"empty", nil, nil, false, false},
+ {"dscp classification", [][]byte{dscp}, nil, true, false},
+ {"pcp remarking", nil, [][]byte{pcp}, false, true},
+ {"ethertype only", [][]byte{ethertype}, nil, false, false},
+ {"both", [][]byte{ethertype, pcp}, [][]byte{dscp}, true, true},
+ } {
+ app, rewr := parseIEEE(ieeeReply(t, tc.app, tc.rewr))
+ if app != tc.wantApp || rewr != tc.wantRewr {
+ t.Errorf("%s: got app %v rewr %v", tc.name, app, rewr)
+ }
+ }
+}
diff --git a/src/yangerd/internal/qosmonitor/qos.go b/src/yangerd/internal/qosmonitor/qos.go
new file mode 100644
index 0000000000..4e940ad232
--- /dev/null
+++ b/src/yangerd/internal/qosmonitor/qos.go
@@ -0,0 +1,98 @@
+package qosmonitor
+
+import (
+ "encoding/json"
+)
+
+// trustOrders are the orders a driver's dcb apptrust accepts, by driver
+// name; the kernel has no query for it. Same table as confd's qos
+// validation.
+var trustOrders = map[string][]string{
+ "sparx5-switch": {"pcp", "dscp", "dscp-pcp"},
+ "lan966x-switch": {"pcp", "dscp", "dscp-pcp"},
+ "mv88e6085": {"pcp", "dscp", "pcp-dscp", "dscp-pcp"},
+}
+
+// port is what the kernel says about one port's QoS.
+type port struct {
+ TxQueues int
+ Driver string
+ App bool // PCP or DSCP entries in the DCB app table
+ Rewr bool // PCP or DSCP entries in the DCB rewrite table
+ Qdiscs json.RawMessage // tc qdisc show
+}
+
+type qdisc struct {
+ Kind string `json:"kind"`
+ Handle string `json:"handle"`
+ Parent string `json:"parent"`
+ Root bool `json:"root"`
+ Offloaded bool `json:"offloaded"`
+ Options json.RawMessage `json:"options"`
+}
+
+// build returns the members the port adds to its interface entry, keyed
+// by module-prefixed node name, or nil when it has none.
+func build(p port) map[string]any {
+ var qdiscs []qdisc
+ json.Unmarshal(p.Qdiscs, &qdiscs)
+
+ qos := map[string]any{}
+ if caps := capabilities(p, qdiscs); len(caps) > 0 {
+ qos["capabilities"] = caps
+ }
+
+ out := map[string]any{}
+ if len(qos) > 0 {
+ out["infix-interfaces:qos"] = qos
+ }
+ if len(out) == 0 {
+ return nil
+ }
+ return out
+}
+
+func capabilities(p port, qdiscs []qdisc) map[string]any {
+ caps := map[string]any{}
+
+ // One class per transmit queue, at most eight. A single queue has
+ // no queue structure to respect, so the kernel's eight classes apply,
+ // which is the model's default and left implicit.
+ if p.TxQueues > 1 && p.TxQueues < 8 {
+ caps["max-traffic-classes"] = p.TxQueues
+ }
+
+ trust := trustOrders[p.Driver]
+ if trust != nil {
+ caps["supported-trust-order"] = trust
+ }
+
+ // Stages the driver runs. ets and tbf report offloaded when the
+ // driver took them.
+ var offload []string
+ if trust != nil {
+ if p.App {
+ offload = append(offload, "classification")
+ }
+ if p.Rewr {
+ offload = append(offload, "remarking")
+ }
+ }
+
+ for _, q := range qdiscs {
+ switch q.Kind {
+ case "ets":
+ if q.Offloaded {
+ offload = append(offload, "transmission-selection")
+ }
+ case "tbf":
+ if q.Root && q.Offloaded {
+ offload = append(offload, "rate-limit")
+ }
+ }
+ }
+ if len(offload) > 0 {
+ caps["offload"] = offload
+ }
+ return caps
+}
diff --git a/src/yangerd/internal/qosmonitor/qos_test.go b/src/yangerd/internal/qosmonitor/qos_test.go
new file mode 100644
index 0000000000..9696d44683
--- /dev/null
+++ b/src/yangerd/internal/qosmonitor/qos_test.go
@@ -0,0 +1,114 @@
+package qosmonitor
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "log/slog"
+ "reflect"
+ "testing"
+
+ "github.com/mdlayher/netlink"
+ "golang.org/x/sys/unix"
+)
+
+func buildJSON(t *testing.T, p port) map[string]any {
+ t.Helper()
+ out := build(p)
+ raw, err := json.Marshal(out)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got map[string]any
+ json.Unmarshal(raw, &got)
+ return got
+}
+
+func TestBuildNothing(t *testing.T) {
+ p := port{TxQueues: 1, Qdiscs: json.RawMessage(`[{"kind":"noqueue","handle":"0:","root":true,"options":{}}]`)}
+ if out := build(p); out != nil {
+ t.Fatalf("want nil, got %v", out)
+ }
+}
+
+func TestBuildSwitchPort(t *testing.T) {
+ p := port{
+ TxQueues: 4,
+ Driver: "sparx5-switch",
+ App: true,
+ Qdiscs: json.RawMessage(`[{"kind":"ets","handle":"8001:","root":true,"offloaded":true,"options":{"bands":4}}]`),
+ }
+ got := buildJSON(t, p)
+ want := map[string]any{"infix-interfaces:qos": map[string]any{
+ "capabilities": map[string]any{
+ "max-traffic-classes": 4.0,
+ "supported-trust-order": []any{"pcp", "dscp", "dscp-pcp"},
+ "offload": []any{"classification", "transmission-selection"},
+ },
+ }}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("got %v\nwant %v", got, want)
+ }
+}
+
+type fakeDCB struct{ app, rewr bool }
+
+func (f fakeDCB) Tables(string) (bool, bool, error) { return f.app, f.rewr, nil }
+
+type fakeBatch map[string]string
+
+func (f fakeBatch) Query(cmd string) (json.RawMessage, error) {
+ if out, ok := f[cmd]; ok {
+ return json.RawMessage(out), nil
+ }
+ return nil, errors.New("command failed")
+}
+
+func TestDrainOnlyOnChange(t *testing.T) {
+ dir := t.TempDir()
+ tc := fakeBatch{"qdisc show dev if7": `[{"kind":"ets","handle":"1:","root":true,"offloaded":true,"options":{"bands":8}}]`}
+ links := `[{"ifname":"e1","ifindex":7,"num_tx_queues":4}]`
+ m := New(tc, fakeDCB{}, func() json.RawMessage { return json.RawMessage(links) },
+ slog.New(slog.NewTextHandler(io.Discard, nil)))
+ m.sysfs = dir
+
+ var calls []map[string]json.RawMessage
+ m.SetOnUpdate(func(c map[string]json.RawMessage) { calls = append(calls, c) })
+
+ m.refreshIndex(7)
+ m.drain()
+ m.Refresh("e1")
+ m.drain()
+ if len(calls) != 1 {
+ t.Fatalf("want one update for unchanged state, got %v", calls)
+ }
+
+ links = `[]`
+ m.Refresh("e1")
+ m.drain()
+ if got, ok := calls[len(calls)-1]["e1"]; len(calls) != 2 || !ok || got != nil {
+ t.Fatalf("want removal, got %v", calls)
+ }
+}
+
+func TestDispatch(t *testing.T) {
+ m := New(fakeBatch{}, fakeDCB{}, func() json.RawMessage { return nil },
+ slog.New(slog.NewTextHandler(io.Discard, nil)))
+
+ ae := netlink.NewAttributeEncoder()
+ ae.String(dcbAttrIfname, "e3")
+ attrs, _ := ae.Encode()
+ m.dispatch(netlink.Message{
+ Header: netlink.Header{Type: unix.RTM_SETDCB},
+ Data: append([]byte{0, 0, 0, 0}, attrs...),
+ })
+ // A filter notification on the TC group is not about qdiscs.
+ m.dispatch(netlink.Message{Header: netlink.Header{Type: unix.RTM_NEWTFILTER}, Data: make([]byte, sizeofTcmsg)})
+
+ if len(m.pending) != 1 {
+ t.Fatalf("want e3 pending, got %v", m.pending)
+ }
+ if _, ok := m.pending["e3"]; !ok {
+ t.Fatalf("want e3 pending, got %v", m.pending)
+ }
+}
diff --git a/src/yangerd/internal/qosmonitor/qosmonitor.go b/src/yangerd/internal/qosmonitor/qosmonitor.go
new file mode 100644
index 0000000000..733ed4cbf9
--- /dev/null
+++ b/src/yangerd/internal/qosmonitor/qosmonitor.go
@@ -0,0 +1,302 @@
+// Package qosmonitor keeps the QoS operational state of each port, its
+// capabilities. The qdisc tree is read through a long-lived `tc` batch
+// process and the DCB tables over rtnetlink, one query per port, on
+// rtnetlink TC and DCB events and on link events.
+package qosmonitor
+
+import (
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/kernelkit/infix/src/yangerd/internal/backoff"
+ "github.com/mdlayher/netlink"
+ "golang.org/x/sys/unix"
+)
+
+const (
+ // settle lets a confd script finish its tc and dcb commands, each
+ // one an event, before the ports are read.
+ settle = 50 * time.Millisecond
+
+ sizeofTcmsg = 20
+ sizeofDcbmsg = 4
+ dcbAttrIfname = 1
+)
+
+// Querier answers one batch command, see ipbatch.Batch.
+type Querier interface {
+ Query(command string) (json.RawMessage, error)
+}
+
+// DCBReader reads a port's DCB tables, see DCB.
+type DCBReader interface {
+ Tables(ifname string) (app, rewr bool, err error)
+}
+
+// link is the part of an `ip -json -d link show` row used here.
+type link struct {
+ Name string `json:"ifname"`
+ Index int `json:"ifindex"`
+ TxQueues int `json:"num_tx_queues"`
+}
+
+// Monitor follows the QoS state of every port.
+type Monitor struct {
+ tc Querier
+ dcb DCBReader
+ links func() json.RawMessage
+ log *slog.Logger
+ onUpdate func(map[string]json.RawMessage)
+
+ sysfs string
+
+ mu sync.Mutex
+ pending map[string]struct{}
+ pendingIdx map[int]struct{}
+ last map[string]string // what onUpdate was last given, by port
+ kick chan struct{}
+}
+
+// New creates a Monitor reading qdiscs through tc and DCB tables through
+// dcb, see DCB, for the interfaces in links, an `ip -json -d link show`
+// dump.
+func New(tc Querier, dcb DCBReader, links func() json.RawMessage, log *slog.Logger) *Monitor {
+ return &Monitor{
+ tc: tc,
+ dcb: dcb,
+ links: links,
+ log: log,
+ sysfs: "/sys/class/net",
+ pending: make(map[string]struct{}),
+ pendingIdx: make(map[int]struct{}),
+ last: make(map[string]string),
+ kick: make(chan struct{}, 1),
+ }
+}
+
+// SetOnUpdate sets the callback given, for every port that changed, its
+// members of the interface entry as a JSON object, nil when it has none.
+func (m *Monitor) SetOnUpdate(fn func(map[string]json.RawMessage)) {
+ m.onUpdate = fn
+}
+
+// Refresh queues a re-read of one port.
+func (m *Monitor) Refresh(ifname string) {
+ m.mu.Lock()
+ m.pending[ifname] = struct{}{}
+ m.mu.Unlock()
+ m.wake()
+}
+
+func (m *Monitor) refreshIndex(index int) {
+ m.mu.Lock()
+ m.pendingIdx[index] = struct{}{}
+ m.mu.Unlock()
+ m.wake()
+}
+
+func (m *Monitor) wake() {
+ select {
+ case m.kick <- struct{}{}:
+ default:
+ }
+}
+
+// Sweep queues a re-read of every port, and of every port reported
+// before, for start and a lost event stream.
+func (m *Monitor) Sweep() {
+ m.mu.Lock()
+ for _, l := range m.linkTable() {
+ m.pending[l.Name] = struct{}{}
+ }
+ for name := range m.last {
+ m.pending[name] = struct{}{}
+ }
+ m.mu.Unlock()
+ m.wake()
+}
+
+// Run serves refresh requests and the rtnetlink stream until ctx is
+// cancelled.
+func (m *Monitor) Run(ctx context.Context) error {
+ go m.worker(ctx)
+ return backoff.Retry(ctx, m.log, "qosmonitor", m.receive)
+}
+
+func (m *Monitor) receive(ctx context.Context) error {
+ conn, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
+ Groups: 1<<(unix.RTNLGRP_TC-1) | 1<<(unix.RTNLGRP_DCB-1),
+ })
+ if err != nil {
+ return fmt.Errorf("dial rtnetlink: %w", err)
+ }
+ defer conn.Close()
+
+ // Receive has no deadline; closing the socket is what ends it.
+ stop := context.AfterFunc(ctx, func() { conn.Close() })
+ defer stop()
+
+ // Whatever changed before the subscription, or while it was down.
+ m.Sweep()
+
+ for {
+ msgs, err := conn.Receive()
+ if err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ return fmt.Errorf("receive rtnetlink message: %w", err)
+ }
+ for _, msg := range msgs {
+ m.dispatch(msg)
+ }
+ }
+}
+
+// dispatch queues a refresh of the port a qdisc or DCB notification is
+// about. Filter and class notifications share the TC group and change
+// nothing reported here.
+func (m *Monitor) dispatch(msg netlink.Message) {
+ switch msg.Header.Type {
+ case unix.RTM_NEWQDISC, unix.RTM_DELQDISC:
+ if len(msg.Data) >= sizeofTcmsg {
+ m.refreshIndex(int(int32(binary.NativeEndian.Uint32(msg.Data[4:8]))))
+ }
+ case unix.RTM_SETDCB, unix.RTM_GETDCB:
+ if name := dcbIfname(msg.Data); name != "" {
+ m.Refresh(name)
+ } else {
+ m.Sweep()
+ }
+ }
+}
+
+func dcbIfname(data []byte) string {
+ if len(data) < sizeofDcbmsg {
+ return ""
+ }
+ ad, err := netlink.NewAttributeDecoder(data[sizeofDcbmsg:])
+ if err != nil {
+ return ""
+ }
+ for ad.Next() {
+ if ad.Type() == dcbAttrIfname {
+ return ad.String()
+ }
+ }
+ return ""
+}
+
+func (m *Monitor) worker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-m.kick:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(settle):
+ }
+ m.drain()
+ }
+}
+
+// drain reads every pending port and hands on what changed, at once.
+func (m *Monitor) drain() {
+ links := make(map[string]link)
+ byIndex := make(map[int]string)
+ for _, l := range m.linkTable() {
+ links[l.Name] = l
+ byIndex[l.Index] = l.Name
+ }
+
+ m.mu.Lock()
+ names := m.pending
+ m.pending = make(map[string]struct{})
+ for index := range m.pendingIdx {
+ if name, ok := byIndex[index]; ok {
+ names[name] = struct{}{}
+ }
+ }
+ m.pendingIdx = make(map[int]struct{})
+ m.mu.Unlock()
+
+ changed := make(map[string]json.RawMessage)
+ for name := range names {
+ var raw json.RawMessage
+ if l, ok := links[name]; ok {
+ if members := build(m.read(l)); members != nil {
+ var err error
+ if raw, err = json.Marshal(members); err != nil {
+ m.log.Warn("qosmonitor: marshal", "ifname", name, "err", err)
+ continue
+ }
+ }
+ }
+
+ m.mu.Lock()
+ prev := m.last[name]
+ if raw == nil {
+ delete(m.last, name)
+ } else {
+ m.last[name] = string(raw)
+ }
+ m.mu.Unlock()
+
+ if prev != string(raw) {
+ changed[name] = raw
+ }
+ }
+
+ if len(changed) > 0 && m.onUpdate != nil {
+ m.onUpdate(changed)
+ }
+}
+
+func (m *Monitor) linkTable() []link {
+ var rows []link
+ json.Unmarshal(m.links(), &rows)
+ return rows
+}
+
+// read asks the kernel about one port.
+func (m *Monitor) read(l link) port {
+ p := port{TxQueues: l.TxQueues, Driver: m.driver(l.Name)}
+
+ // By index: tc caches names for the life of the process, so a name
+ // reused by a new interface would resolve to the old one.
+ var err error
+ if p.Qdiscs, err = m.tc.Query("qdisc show dev if" + strconv.Itoa(l.Index)); err != nil {
+ m.log.Debug("qosmonitor: tc", "ifname", l.Name, "err", err)
+ }
+ if trustOrders[p.Driver] != nil {
+ if p.App, p.Rewr, err = m.dcb.Tables(l.Name); err != nil {
+ m.log.Debug("qosmonitor: dcb", "ifname", l.Name, "err", err)
+ }
+ }
+ return p
+}
+
+func (m *Monitor) driver(ifname string) string {
+ data, err := os.ReadFile(filepath.Join(m.sysfs, ifname, "device", "uevent"))
+ if err != nil {
+ return ""
+ }
+ for _, line := range strings.Split(string(data), "\n") {
+ if drv, ok := strings.CutPrefix(line, "DRIVER="); ok {
+ return strings.TrimSpace(drv)
+ }
+ }
+ return ""
+}
diff --git a/test/case/interfaces/Readme.adoc b/test/case/interfaces/Readme.adoc
index df9455f7c3..853be45ab5 100644
--- a/test/case/interfaces/Readme.adoc
+++ b/test/case/interfaces/Readme.adoc
@@ -12,7 +12,7 @@ Tests verifying interface configuration and management:
- Linux bridge creation, STP, and VLAN handling
- Link aggregation (LAG) setup and failover behavior
- IGMP multicast group management and forwarding
- - QoS traffic classification and priority handling
+ - QoS traffic classification, traffic classes, and transmission selection
- Interface enable/disable state management
- Virtual Ethernet (VETH) pair lifecycle management
- GRE tunneling configuration and data plane operation
@@ -118,6 +118,26 @@ include::vlan_qos/Readme.adoc[]
<<<
+include::qos_traffic_class/Readme.adoc[]
+
+<<<
+
+include::qos_validation/Readme.adoc[]
+
+<<<
+
+include::qos_ingress_classify/Readme.adoc[]
+
+<<<
+
+include::qos_classify_dscp/Readme.adoc[]
+
+<<<
+
+include::qos_factory_default/Readme.adoc[]
+
+<<<
+
include::verify_all_interface_types/Readme.adoc[]
<<<
diff --git a/test/case/interfaces/all.yaml b/test/case/interfaces/all.yaml
index e1ad75b43a..dfb0dce379 100644
--- a/test/case/interfaces/all.yaml
+++ b/test/case/interfaces/all.yaml
@@ -56,6 +56,9 @@
- name: Layer-2 IPv6 Multicast Filtering Tests
suite: mld.yaml
+- name: Quality of Service Tests
+ suite: qos.yaml
+
- name: Layer-2/3 Tunnel Tests
suite: tunnels.yaml
diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml
new file mode 100644
index 0000000000..e048105b8f
--- /dev/null
+++ b/test/case/interfaces/qos.yaml
@@ -0,0 +1,33 @@
+---
+- name: QoS Traffic Classes and Transmission Selection
+ case: qos_traffic_class/test.py
+
+- name: QoS Configuration Validation
+ case: qos_validation/test.py
+
+- name: QoS Ingress Classification and Egress Remarking
+ case: qos_ingress_classify/test.py
+
+- name: QoS DSCP Classification and Remarking End to End
+ case: qos_classify_dscp/test.py
+
+- name: QoS Defaults Out of the Box
+ case: qos_factory_default/test.py
+
+- name: QoS Bridge Classification and Remarking
+ case: qos_bridge_classify/test.py
+
+- name: QoS Queue Assignment
+ case: qos_queue_assignment/test.py
+
+- name: QoS Rate Limit
+ case: qos_rate_limit/test.py
+
+- name: QoS Strict Priority
+ case: qos_strict_priority/test.py
+
+- name: QoS Weighted Sharing
+ case: qos_weighted_sharing/test.py
+
+- name: QoS Mixed Selection
+ case: qos_mixed_selection/test.py
diff --git a/test/case/interfaces/qos_bridge_classify/Readme.adoc b/test/case/interfaces/qos_bridge_classify/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_bridge_classify/test.adoc b/test/case/interfaces/qos_bridge_classify/test.adoc
new file mode 100644
index 0000000000..e4842de69d
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/test.adoc
@@ -0,0 +1,44 @@
+=== QoS Bridge Classification and Remarking
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_bridge_classify]
+
+==== Description
+
+Bridge two ports and send frames straight through the switch, so that on
+a switch chip the traffic never passes the CPU and only the hardware
+tables can classify and remark it. The DSCP and PCP on the wire at the
+egress port reveal the priority the ingress port assigned:
+
+ - trust dscp: untagged IP frames on VLAN 20, DSCP 46 (EF) to priority 5,
+ DSCP 4 unknown to the preset to the default priority 1
+ - trust pcp with a custom map: tagged frames on VLAN 10, PCP 3 to
+ priority 6, PCP 0 to priority 4, PCP 7 unmapped to the default
+ priority 2
+ - remark off: DSCP leaves as it came in, and so does the PCP unless the
+ switch fabric encodes it from the priority, which its rewrite table
+ then still shows
+
+With remarking on, the egress port writes the class selector of the
+priority as DSCP and, where the driver offloads remarking, the priority
+as PCP; PCP remarking has no software path, so on other ports only the
+DSCP is checked. The test is skipped on a switch whose driver cannot
+offload classification, since frames the fabric forwards never see the
+kernel's rules.
+
+==== Topology
+
+image::topology.svg[QoS Bridge Classification and Remarking topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Configure a VLAN bridge, VLAN 10 tagged and VLAN 20 untagged on ingress
+. Set up host namespaces, VLAN 10 tagged on both, VLAN 20 untagged on the sender
+. Verify classification and remarking are offloaded
+. Send untagged IP frames with trust dscp, verify PCP and DSCP from priority
+. Switch to trust pcp with a custom map and default priority 2
+. Send tagged frames, verify PCP and DSCP from the mapped priority
+. Turn remarking off
+. Send tagged frames with DSCP 46, verify PCP and DSCP are untouched
+
+
diff --git a/test/case/interfaces/qos_bridge_classify/test.py b/test/case/interfaces/qos_bridge_classify/test.py
new file mode 100755
index 0000000000..a7baae1e1e
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/test.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""
+QoS Bridge Classification and Remarking
+
+Bridge two ports and send frames straight through the switch, so that on
+a switch chip the traffic never passes the CPU and only the hardware
+tables can classify and remark it. The DSCP and PCP on the wire at the
+egress port reveal the priority the ingress port assigned:
+
+ - trust dscp: untagged IP frames on VLAN 20, DSCP 46 (EF) to priority 5,
+ DSCP 4 unknown to the preset to the default priority 1
+ - trust pcp with a custom map: tagged frames on VLAN 10, PCP 3 to
+ priority 6, PCP 0 to priority 4, PCP 7 unmapped to the default
+ priority 2
+ - remark off: DSCP leaves as it came in, and so does the PCP unless the
+ switch fabric encodes it from the priority, which its rewrite table
+ then still shows
+
+With remarking on, the egress port writes the class selector of the
+priority as DSCP and, where the driver offloads remarking, the priority
+as PCP; PCP remarking has no software path, so on other ports only the
+DSCP is checked. The test is skipped on a switch whose driver cannot
+offload classification, since frames the fabric forwards never see the
+kernel's rules.
+"""
+import re
+import infamy
+from infamy.util import until
+
+# DSCP cases with trust dscp: (dscp, expected priority)
+DSCP_CASES = ((46, 5), (0, 0), (26, 3), (4, 1))
+# PCP cases with trust pcp and a custom map: (pcp, expected priority)
+PCP_CASES = ((3, 6), (0, 4), (7, 2))
+
+
+def qos_config(target, td0, td1, ingress, remark):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": td0, "infix-interfaces:qos": {"ingress": ingress}},
+ {"name": td1, "infix-interfaces:qos": {"egress": {"remark": remark}}},
+ ]
+ }
+ }})
+
+
+def parse(packets):
+ """Return {icmp id: (vid, pcp, dscp)} from tcpdump -e -v output"""
+ seen = {}
+ for m in re.finditer(r"vlan (\d+), p (\d+), .*?\(tos 0x([0-9a-f]+).*?"
+ r"ICMP echo request, id (\d+)", packets, re.S):
+ seen[int(m.group(4))] = (int(m.group(1)), int(m.group(2)), int(m.group(3), 16) >> 2)
+ return seen
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ ingress = infamy.capability.Port(target, td0, tgtssh)
+ egress = infamy.capability.Port(target, td1, tgtssh)
+ dcb = bool(ingress.trust_orders)
+ print(f"{td0}: DCB {'supported' if dcb else 'not supported'}, "
+ f"switch port: {ingress.switched}")
+ if ingress.switched and not dcb:
+ test.skip(f"{td0} forwards in hardware and its driver has no DCB tables")
+
+ with test.step("Configure a VLAN bridge, VLAN 10 tagged and VLAN 20 untagged on ingress"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": "br0",
+ "type": "infix-if-type:bridge",
+ "enabled": True,
+ "bridge": {
+ "vlans": {
+ "vlan": [
+ {"vid": 10, "tagged": [td0, td1]},
+ {"vid": 20, "untagged": [td0], "tagged": [td1]},
+ ]
+ }
+ }
+ },
+ {
+ "name": td0,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"pvid": 20, "bridge": "br0"},
+ "infix-interfaces:qos": {
+ "ingress": {
+ "trust": "dscp",
+ "default-priority": 1,
+ "dscp-map": {"preset": "ietf"},
+ }
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "egress": {"remark": {"pcp": "from-priority", "dscp": "from-priority"}}
+ }
+ },
+ ]
+ }
+ }})
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up host namespaces, VLAN 10 tagged on both, VLAN 20 untagged on the sender"):
+ ns0.runsh("""
+ set -ex
+ ip link set iface up
+ ip addr add 192.168.20.1/24 dev iface
+ ip link add dev vlan10 link iface up type vlan id 10
+ ip addr add 192.168.10.1/24 dev vlan10
+ """)
+ ns1.runsh("""
+ set -ex
+ ip link set iface up
+ ip link add dev vlan10 link iface up type vlan id 10
+ ip addr add 192.168.10.2/24 dev vlan10
+ ip link add dev vlan20 link iface up type vlan id 20
+ ip addr add 192.168.20.2/24 dev vlan20
+ """)
+ ns0.must_reach("192.168.20.2")
+ ns0.must_reach("192.168.10.2")
+
+ if dcb:
+ with test.step("Verify classification and remarking are offloaded"):
+ until(lambda: ingress.offloads("classification"))
+ until(lambda: egress.offloads("remarking"))
+ remark_hw = egress.offloads("remarking")
+
+ def expect(seen, ident, vid, pcp, dscp, what):
+ """Check one captured echo request, PCP only where the driver remarks it"""
+ assert ident in seen, f"{what}: no echo request captured"
+ got = seen[ident]
+ want = (vid, pcp if remark_hw else got[1], dscp)
+ assert got == want, f"{what}: got (vid, pcp, dscp) {got}, expected {want}"
+
+ def capture(send):
+ pcap = ns1.pcap("icmp[icmptype] == icmp-echo")
+ with pcap:
+ send()
+ packets = pcap.tcpdump("-e -v")
+ print(packets)
+ return parse(packets)
+
+ def set_pcp(pcp):
+ """Tag everything the sender puts on VLAN 10 with this PCP, whatever its TOS"""
+ qmap = " ".join(f"{prio}:{pcp}" for prio in range(8))
+ ns0.runsh(f"ip link set dev vlan10 type vlan egress-qos-map {qmap}")
+
+ def send_dscp():
+ for n, (dscp, _) in enumerate(DSCP_CASES):
+ ns0.runsh(f"ping -c1 -w2 -Q {dscp << 2} -e {100 + n} 192.168.20.2")
+
+ def send_pcp():
+ for n, (pcp, _) in enumerate(PCP_CASES):
+ set_pcp(pcp)
+ ns0.runsh(f"ping -c1 -w2 -e {200 + n} 192.168.10.2")
+
+ with test.step("Send untagged IP frames with trust dscp, verify PCP and DSCP from priority"):
+ seen = capture(send_dscp)
+ for n, (dscp, prio) in enumerate(DSCP_CASES):
+ expect(seen, 100 + n, 20, prio, prio << 3, f"DSCP {dscp}")
+
+ with test.step("Switch to trust pcp with a custom map and default priority 2"):
+ qos_config(target, td0, td1, {
+ "trust": "pcp",
+ "default-priority": 2,
+ "pcp-map": {
+ "entry": [
+ {"pcp": 3, "dei": "false", "priority": 6},
+ {"pcp": 3, "dei": "true", "priority": 6},
+ {"pcp": 0, "dei": "false", "priority": 4},
+ {"pcp": 0, "dei": "true", "priority": 4},
+ ]
+ },
+ }, {"pcp": "from-priority", "dscp": "from-priority"})
+ ns0.must_reach("192.168.10.2")
+
+ with test.step("Send tagged frames, verify PCP and DSCP from the mapped priority"):
+ seen = capture(send_pcp)
+ for n, (pcp, prio) in enumerate(PCP_CASES):
+ expect(seen, 200 + n, 10, prio, prio << 3, f"PCP {pcp}")
+
+ with test.step("Turn remarking off"):
+ qos_config(target, td0, td1, {"trust": "pcp", "default-priority": 2},
+ {"pcp": "none", "dscp": "none"})
+ ns0.must_reach("192.168.10.2")
+
+ with test.step("Send tagged frames with DSCP 46, verify PCP and DSCP are untouched"):
+ def send_plain():
+ for n, (pcp, _) in enumerate(PCP_CASES):
+ set_pcp(pcp)
+ ns0.runsh(f"ping -c1 -w2 -Q {46 << 2} -e {300 + n} 192.168.10.2")
+
+ # A fabric that encodes the PCP from the priority keeps its
+ # table with remarking off, and says so in the rewrite table
+ rewr = tgtssh.runsh(f"dcb rewr show dev {td1}").stdout
+ encoded = "prio-pcp" in rewr
+ print(f"{td1} encodes PCP from priority: {encoded}")
+
+ seen = capture(send_plain)
+ for n, (pcp, prio) in enumerate(PCP_CASES):
+ assert 300 + n in seen, f"no echo request with PCP {pcp} captured"
+ want = (10, prio if encoded else pcp, 46)
+ assert seen[300 + n] == want, f"PCP {pcp}: got {seen[300 + n]}, expected {want}"
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_bridge_classify/topology.dot b/test/case/interfaces/qos_bridge_classify/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_bridge_classify/topology.svg b/test/case/interfaces/qos_bridge_classify/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_classify_dscp/Readme.adoc b/test/case/interfaces/qos_classify_dscp/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_classify_dscp/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_classify_dscp/test.adoc b/test/case/interfaces/qos_classify_dscp/test.adoc
new file mode 100644
index 0000000000..148b9d538b
--- /dev/null
+++ b/test/case/interfaces/qos_classify_dscp/test.adoc
@@ -0,0 +1,44 @@
+=== QoS DSCP Classification and Remarking End to End
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_classify_dscp]
+
+==== Description
+
+Send IP packets with different DSCP values into a routed port that trusts
+DSCP with the ietf preset, and route them out over a VLAN interface whose
+egress PCP is derived from the internal priority. The PCP on the wire
+then reveals the priority the classifier assigned:
+
+ DSCP 0 (CS0) -> priority 0
+ DSCP 8 (CS1) -> priority 1
+ DSCP 18 (AF21) -> priority 2
+ DSCP 26 (AF31) -> priority 3
+ DSCP 34 (AF41) -> priority 4
+ DSCP 46 (EF) -> priority 5
+ DSCP 48 (CS6) -> priority 6
+ DSCP 56 (CS7) -> priority 7
+ DSCP 4 -> priority 0, not in the preset, port default
+
+Then enable DSCP remarking on the egress port and repeat: every packet
+must leave with the class selector of its priority, CS0 to CS7, e.g. EF
+in, CS5 out.
+
+Works on any port: classification and remarking run in the switch fabric
+where the driver supports them and in the kernel otherwise, and routed
+traffic passes the kernel in both cases.
+
+==== Topology
+
+image::topology.svg[QoS DSCP Classification and Remarking End to End topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Configure routed ingress port trusting DSCP and VLAN egress from priority
+. Set up host namespaces on both sides
+. Send ICMP echo with DSCP {dscp}, {what} {prio}
+. Verify the PCP of each echo request matches its DSCP class
+. Enable DSCP remarking from priority on the egress port
+. Verify each echo request leaves with the class selector of its priority
+
+
diff --git a/test/case/interfaces/qos_classify_dscp/test.py b/test/case/interfaces/qos_classify_dscp/test.py
new file mode 100755
index 0000000000..fadc3a815b
--- /dev/null
+++ b/test/case/interfaces/qos_classify_dscp/test.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""
+QoS DSCP Classification and Remarking End to End
+
+Send IP packets with different DSCP values into a routed port that trusts
+DSCP with the ietf preset, and route them out over a VLAN interface whose
+egress PCP is derived from the internal priority. The PCP on the wire
+then reveals the priority the classifier assigned:
+
+ DSCP 0 (CS0) -> priority 0
+ DSCP 8 (CS1) -> priority 1
+ DSCP 18 (AF21) -> priority 2
+ DSCP 26 (AF31) -> priority 3
+ DSCP 34 (AF41) -> priority 4
+ DSCP 46 (EF) -> priority 5
+ DSCP 48 (CS6) -> priority 6
+ DSCP 56 (CS7) -> priority 7
+ DSCP 4 -> priority 0, not in the preset, port default
+
+Then enable DSCP remarking on the egress port and repeat: every packet
+must leave with the class selector of its priority, CS0 to CS7, e.g. EF
+in, CS5 out.
+
+Works on any port: classification and remarking run in the switch fabric
+where the driver supports them and in the kernel otherwise, and routed
+traffic passes the kernel in both cases.
+"""
+import re
+import infamy
+
+# DSCP to expected priority per the ietf preset, with a default fallback
+CASES = ((0, 0), (8, 1), (18, 2), (26, 3), (34, 4), (46, 5), (48, 6), (56, 7), (4, 0))
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ with test.step("Configure routed ingress port trusting DSCP and VLAN egress from priority"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": td0,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{"ip": "192.168.10.1", "prefix-length": 24}]
+ },
+ "infix-interfaces:qos": {
+ "ingress": {
+ "trust": "dscp",
+ "default-priority": 0,
+ "dscp-map": {"preset": "ietf"},
+ }
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ },
+ {
+ "name": "vlan11",
+ "type": "infix-if-type:vlan",
+ "vlan": {
+ "id": 11,
+ "lower-layer-if": td1,
+ "egress-qos": {"pcp": "from-priority"},
+ },
+ "ipv4": {
+ "forwarding": True,
+ "address": [{"ip": "192.168.11.1", "prefix-length": 24}]
+ }
+ }
+ ]
+ }
+ }})
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up host namespaces on both sides"):
+ ns0.runsh("""
+ set -ex
+ ip link set iface up
+ ip addr add 192.168.10.2/24 dev iface
+ """)
+ ns0.addroute("default", "192.168.10.1")
+
+ ns1.runsh("""
+ set -ex
+ ip link set iface up
+ ip link add dev vlan11 link iface up type vlan id 11
+ ip addr add 192.168.11.2/24 dev vlan11
+ """)
+ ns1.addroute("default", "192.168.11.1")
+
+ ns0.must_reach("192.168.11.2")
+
+ def capture(what):
+ """Ping once per case with the case index as ICMP id, return {id: (pcp, dscp)}"""
+ pcap = ns1.pcap("vlan 11 and icmp[icmptype] == icmp-echo")
+ with pcap:
+ for n, (dscp, prio) in enumerate(CASES):
+ with test.step(f"Send ICMP echo with DSCP {dscp}, {what} {prio}"):
+ ns0.runsh(f"ping -c1 -w2 -Q {dscp << 2} -e {100 + n} 192.168.11.2")
+ packets = pcap.tcpdump("-e -v")
+ print(packets)
+
+ seen = {}
+ for m in re.finditer(r"vlan 11, p (\d+), .*?\(tos 0x([0-9a-f]+).*?"
+ r"ICMP echo request, id (\d+)", packets, re.S):
+ seen[int(m.group(3))] = (int(m.group(1)), int(m.group(2), 16) >> 2)
+ return seen
+
+ seen = capture("expect PCP")
+ with test.step("Verify the PCP of each echo request matches its DSCP class"):
+ for n, (dscp, prio) in enumerate(CASES):
+ assert 100 + n in seen, f"no echo request with DSCP {dscp} captured"
+ assert seen[100 + n] == (prio, dscp), f"DSCP {dscp}: got {seen[100 + n]}"
+
+ with test.step("Enable DSCP remarking from priority on the egress port"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": td1,
+ "infix-interfaces:qos": {
+ "egress": {"remark": {"dscp": "from-priority"}}
+ }
+ }]
+ }
+ }})
+
+ # Applying the change may reprogram the port; wait for the path
+ ns0.must_reach("192.168.11.2")
+
+ seen = capture("expect CS")
+ with test.step("Verify each echo request leaves with the class selector of its priority"):
+ for n, (dscp, prio) in enumerate(CASES):
+ assert 100 + n in seen, f"no echo request with DSCP {dscp} captured"
+ assert seen[100 + n] == (prio, prio << 3), f"DSCP {dscp}: got {seen[100 + n]}"
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_classify_dscp/topology.dot b/test/case/interfaces/qos_classify_dscp/topology.dot
new file mode 120000
index 0000000000..7689a67782
--- /dev/null
+++ b/test/case/interfaces/qos_classify_dscp/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x3.dot
\ No newline at end of file
diff --git a/test/case/interfaces/qos_classify_dscp/topology.svg b/test/case/interfaces/qos_classify_dscp/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_classify_dscp/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_factory_default/Readme.adoc b/test/case/interfaces/qos_factory_default/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_factory_default/test.adoc b/test/case/interfaces/qos_factory_default/test.adoc
new file mode 100644
index 0000000000..f20cb09f2e
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/test.adoc
@@ -0,0 +1,32 @@
+=== QoS Defaults Out of the Box
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_factory_default]
+
+==== Description
+
+An interface without any qos configuration is fully specified by the
+model's defaults, and every physical port runs them from boot:
+
+ - trust PCP, the tag of VLAN-tagged frames, with default priority 0
+ for everything else
+ - one traffic class per transmit queue, eight on a single-queue port,
+ mapped per IEEE 802.1Q-2022 Table 8-5 with strict priority throughout
+
+Verify that the running configuration carries no qos container, that the
+class count follows the queue count rule, that the root qdisc carries the
+Table 8-5 map for it, and that classification is in place: in the DCB
+tables on a port whose driver has them, as tc flower rules otherwise.
+
+==== Topology
+
+image::topology.svg[QoS Defaults Out of the Box topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Verify the running configuration has no qos container
+. Verify the class count is the queue count, or eight for one queue
+. Verify the root qdisc carries the Table 8-5 map, all strict
+. Verify classification trusts PCP by default
+
+
diff --git a/test/case/interfaces/qos_factory_default/test.py b/test/case/interfaces/qos_factory_default/test.py
new file mode 100755
index 0000000000..0698a52721
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/test.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""
+QoS Defaults Out of the Box
+
+An interface without any qos configuration is fully specified by the
+model's defaults, and every physical port runs them from boot:
+
+ - trust PCP, the tag of VLAN-tagged frames, with default priority 0
+ for everything else
+ - one traffic class per transmit queue, eight on a single-queue port,
+ mapped per IEEE 802.1Q-2022 Table 8-5 with strict priority throughout
+
+Verify that the running configuration carries no qos container, that the
+class count follows the queue count rule, that the root qdisc carries the
+Table 8-5 map for it, and that classification is in place: in the DCB
+tables on a port whose driver has them, as tc flower rules otherwise.
+"""
+import json
+import infamy
+from infamy.util import until
+
+TABLE_8_5 = {
+ 2: [0, 0, 0, 0, 1, 1, 1, 1],
+ 3: [0, 0, 0, 0, 1, 1, 2, 2],
+ 4: [0, 0, 1, 1, 2, 2, 3, 3],
+ 5: [0, 0, 1, 1, 2, 2, 3, 4],
+ 6: [1, 0, 2, 2, 3, 3, 4, 5],
+ 7: [1, 0, 2, 3, 4, 4, 5, 6],
+ 8: [1, 0, 2, 3, 4, 5, 6, 7],
+}
+
+
+def running_qos(target, port):
+ running = target.get_config_dict("/ietf-interfaces:interfaces")
+ for iface in running["interfaces"]["interface"]:
+ if iface["name"] == port:
+ return iface.get("qos") or iface.get("infix-interfaces:qos")
+ return None
+
+
+def root_qdisc(ssh, port):
+ out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout
+ for qdisc in json.loads(out or "[]"):
+ if qdisc.get("root"):
+ return qdisc
+ return None
+
+
+def tx_queues(ssh, port):
+ out = ssh.runsh(f"ls /sys/class/net/{port}/queues").stdout
+ return len([q for q in out.split() if q.startswith("tx-")])
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, port = env.ltop.xlate("target", "data")
+
+ with test.step("Verify the running configuration has no qos container"):
+ assert running_qos(target, port) is None, f"{port} has qos configuration"
+
+ with test.step("Verify the class count is the queue count, or eight for one queue"):
+ caps = infamy.capability.Port(target, port, tgtssh).caps
+ queues = tx_queues(tgtssh, port)
+ expected = min(queues, 8) if queues > 1 else 8
+ print(f"{port}: {queues} tx queues, capabilities {caps}")
+ assert caps.get("max-traffic-classes", 8) == expected, caps
+ num_tc = expected
+
+ with test.step("Verify the root qdisc carries the Table 8-5 map, all strict"):
+ def default_table():
+ qdisc = root_qdisc(tgtssh, port)
+ if not qdisc:
+ return False
+ opts = qdisc.get("options", {})
+ if qdisc["kind"] == "ets":
+ return (opts.get("bands") == num_tc and opts.get("strict") == num_tc and
+ opts.get("priomap", [])[:8] == [num_tc - 1 - tc for tc in TABLE_8_5[num_tc]])
+ return False
+ until(default_table)
+ print(json.dumps(root_qdisc(tgtssh, port)))
+
+ with test.step("Verify classification trusts PCP by default"):
+ if caps.get("supported-trust-order"):
+ out = tgtssh.runsh(f"dcb apptrust show dev {port}").stdout
+ assert out.replace(":", "").split() == ["order", "pcp"], out
+ else:
+ out = tgtssh.runsh(f"tc -j filter show dev {port} ingress").stdout
+ keys = [f["options"].get("keys", {}) for f in json.loads(out or "[]") if f.get("options")]
+ assert sum(1 for k in keys if "vlan_prio" in k) == 8, keys
+ assert not any("ip_tos" in k for k in keys), keys
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_factory_default/topology.dot b/test/case/interfaces/qos_factory_default/topology.dot
new file mode 120000
index 0000000000..4f53d15af5
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x2.dot
\ No newline at end of file
diff --git a/test/case/interfaces/qos_factory_default/topology.svg b/test/case/interfaces/qos_factory_default/topology.svg
new file mode 100644
index 0000000000..ff3d246be6
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/topology.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_ingress_classify/Readme.adoc b/test/case/interfaces/qos_ingress_classify/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_ingress_classify/test.adoc b/test/case/interfaces/qos_ingress_classify/test.adoc
new file mode 100644
index 0000000000..c4f0abf353
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/test.adoc
@@ -0,0 +1,42 @@
+=== QoS Ingress Classification and Egress Remarking
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_ingress_classify]
+
+==== Description
+
+Configure the trust order, default priority, the standard PCP and DSCP
+presets, and remarking on transmit, then verify the result:
+
+ - trust dscp-pcp, DSCP first then PCP
+ - default priority 2
+ - PCP preset ieee: PCP n to priority n
+ - DSCP preset ietf: RFC 4594 groups, e.g. EF (46) to priority 5
+ - remark pcp and dscp from priority: priority 5 to PCP 5 and DSCP 40
+
+On a port whose driver has DCB operations the kernel's DCB tables must
+match, and the operational datastore reports classification and
+remarking as offloaded. On any other port the same classification must
+be installed as tc flower rules on the port's ingress, in trust order,
+with a catch-all for the default priority, and DSCP remarking as pedit
+rules on its egress; PCP remarking needs driver support.
+Removing the configuration returns the port to the defaults: trust pcp
+with default priority 0, and no remarking.
+
+==== Topology
+
+image::topology.svg[QoS Ingress Classification and Egress Remarking topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Configure trust dscp-pcp, default priority 2, presets, and remarking
+. Verify dcb apptrust order is dscp pcp
+. Verify dcb app default priority and PCP and DSCP maps
+. Verify dcb rewr priority to PCP and DSCP maps
+. Verify classification and remarking are reported as offloaded
+. Verify tc flower rules: DSCP block before PCP block, then default
+. Verify DSCP remarking as pedit rules on egress
+. Verify classification is not reported as offloaded
+. Remove qos configuration and verify the defaults are back
+
+
diff --git a/test/case/interfaces/qos_ingress_classify/test.py b/test/case/interfaces/qos_ingress_classify/test.py
new file mode 100755
index 0000000000..5d7c534783
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/test.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+"""
+QoS Ingress Classification and Egress Remarking
+
+Configure the trust order, default priority, the standard PCP and DSCP
+presets, and remarking on transmit, then verify the result:
+
+ - trust dscp-pcp, DSCP first then PCP
+ - default priority 2
+ - PCP preset ieee: PCP n to priority n
+ - DSCP preset ietf: RFC 4594 groups, e.g. EF (46) to priority 5
+ - remark pcp and dscp from priority: priority 5 to PCP 5 and DSCP 40
+
+On a port whose driver has DCB operations the kernel's DCB tables must
+match, and the operational datastore reports classification and
+remarking as offloaded. On any other port the same classification must
+be installed as tc flower rules on the port's ingress, in trust order,
+with a catch-all for the default priority, and DSCP remarking as pedit
+rules on its egress; PCP remarking needs driver support.
+Removing the configuration returns the port to the defaults: trust pcp
+with default priority 0, and no remarking.
+"""
+import json
+import re
+import infamy
+from infamy.util import until
+
+
+def dscp_num(name):
+ """dcb prints DSCP by name when it knows one: CS1, AF21, EF ..."""
+ if name.startswith("CS"):
+ return int(name[2:]) * 8
+ if name.startswith("AF"):
+ return int(name[2]) * 8 + int(name[3]) * 2
+ if name == "EF":
+ return 46
+ return int(name)
+
+
+def dcb_tokens(ssh, cmd, port):
+ """Return {table: [tokens]} from dcb ... show dev PORT text output, DSCP as numbers"""
+ out = ssh.runsh(f"dcb {cmd} show dev {port}").stdout
+ tables = {}
+ for line in out.splitlines():
+ name, _, rest = line.partition(" ")
+ name = name.rstrip(":")
+ if not name:
+ continue
+ tokens = rest.split()
+ if name == "dscp-prio":
+ tokens = [f"{dscp_num(k)}:{v}" for k, v in (t.split(":") for t in tokens)]
+ elif name == "prio-dscp":
+ tokens = [f"{k}:{dscp_num(v)}" for k, v in (t.split(":") for t in tokens)]
+ tables[name] = tokens
+ return tables
+
+
+def skbedit_priority(act):
+ """tc prints the priority as a classid: 'none' for 0, else e.g. ':5' in hex"""
+ prio = str(act["priority"])
+ if prio == "none":
+ return 0
+ return int(prio.rsplit(":", 1)[-1] or "0", 16)
+
+
+def flower_rules(ssh, port):
+ """Return [(pref, keys, priority)] for skbedit filters on the port's ingress"""
+ out = ssh.runsh(f"tc -j filter show dev {port} ingress").stdout
+ rules = []
+ for flt in json.loads(out or "[]"):
+ opts = flt.get("options")
+ if not opts:
+ continue
+ for act in opts.get("actions", []):
+ if act.get("kind") == "skbedit" and "priority" in act:
+ rules.append((flt["pref"], opts.get("keys", {}), skbedit_priority(act)))
+ return rules
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, port = env.ltop.xlate("target", "data")
+ cap = infamy.capability.Port(target, port, tgtssh)
+ dcb = bool(cap.trust_orders)
+ print(f"{port}: DCB {'supported' if dcb else 'not supported'}")
+
+ with test.step("Configure trust dscp-pcp, default priority 2, presets, and remarking"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "enabled": True,
+ "infix-interfaces:qos": {
+ "ingress": {
+ "trust": "dscp-pcp",
+ "default-priority": 2,
+ "pcp-map": {"preset": "ieee"},
+ "dscp-map": {"preset": "ietf"},
+ },
+ "egress": {
+ "remark": {"pcp": "from-priority", "dscp": "from-priority"}
+ }
+ }
+ }]
+ }
+ }})
+
+ if dcb:
+ with test.step("Verify dcb apptrust order is dscp pcp"):
+ until(lambda: dcb_tokens(tgtssh, "apptrust", port).get("order") == ["dscp", "pcp"])
+
+ with test.step("Verify dcb app default priority and PCP and DSCP maps"):
+ app = dcb_tokens(tgtssh, "app", port)
+ print(app)
+ assert app.get("default-prio") == ["2"], f"default-prio {app.get('default-prio')}"
+ pcp = set(app.get("pcp-prio", []))
+ for want in ("0nd:0", "1nd:1", "0de:0", "1de:1", "7nd:7", "7de:7"):
+ assert want in pcp, f"missing {want} in pcp-prio {sorted(pcp)}"
+ dscp = set(app.get("dscp-prio", []))
+ for want in ("0:0", "8:1", "26:3", "46:5", "48:6", "56:7"):
+ assert want in dscp, f"missing {want} in dscp-prio {sorted(dscp)}"
+
+ with test.step("Verify dcb rewr priority to PCP and DSCP maps"):
+ rewr = dcb_tokens(tgtssh, "rewr", port)
+ print(rewr)
+ pcp = set(rewr.get("prio-pcp", []))
+ for want in ("0:0nd", "5:5nd", "7:7nd"):
+ assert want in pcp, f"missing {want} in prio-pcp {sorted(pcp)}"
+ dscp = set(rewr.get("prio-dscp", []))
+ for want in ("0:0", "1:8", "5:40", "7:56"):
+ assert want in dscp, f"missing {want} in prio-dscp {sorted(dscp)}"
+
+ with test.step("Verify classification and remarking are reported as offloaded"):
+ until(lambda: {"classification", "remarking"} <= set(cap.offload))
+ else:
+ with test.step("Verify tc flower rules: DSCP block before PCP block, then default"):
+ until(lambda: len(flower_rules(tgtssh, port)) > 0)
+ rules = flower_rules(tgtssh, port)
+ print(f"{len(rules)} skbedit rules")
+
+ # ip_tos is printed as value/mask, e.g. 0xb8/0xfc; key on the DSCP
+ dscp = {}
+ for r in rules:
+ if "ip_tos" in r[1]:
+ tos = int(str(r[1]["ip_tos"]).split("/")[0], 0)
+ dscp.setdefault(tos >> 2, set()).add(r[2])
+ pcp = {r[1]["vlan_prio"]: r for r in rules if "vlan_prio" in r[1]}
+ dflt = [r for r in rules if not r[1]]
+ print(f"dscp {dscp}\npcp {pcp}\ndefault {dflt}")
+
+ assert dscp.get(46) == {5}, f"EF: {dscp.get(46)}"
+ assert dscp.get(8) == {1}, f"CS1: {dscp.get(8)}"
+ assert all(pcp[p][2] == p for p in range(8)), pcp
+ assert dflt and dflt[0][2] == 2, dflt
+ # four variants per codepoint: IPv4, IPv6, tagged IPv4, tagged IPv6
+ assert sum(1 for r in rules if "ip_tos" in r[1]) == 4 * len(dscp), len(rules)
+
+ dscp_pref = {r[0] for r in rules if "ip_tos" in r[1]}
+ pcp_pref = {r[0] for r in pcp.values()}
+ assert max(dscp_pref) < min(pcp_pref) < dflt[0][0], (dscp_pref, pcp_pref, dflt)
+
+ with test.step("Verify DSCP remarking as pedit rules on egress"):
+ # tc -j is not valid JSON for basic filters with ematches, so count in text
+ out = tgtssh.runsh(f"tc filter show dev {port} egress").stdout
+ pedits = [l for l in out.splitlines() if re.match(r"\s*action order \d+:\s+pedit", l)]
+ assert len(pedits) == 32, f"{len(pedits)} pedit rules"
+
+ with test.step("Verify classification is not reported as offloaded"):
+ assert not cap.offloads("classification")
+
+ with test.step("Remove qos configuration and verify the defaults are back"):
+ target.delete_xpath(f"/ietf-interfaces:interfaces/interface[name='{port}']"
+ "/infix-interfaces:qos")
+ if dcb:
+ until(lambda: dcb_tokens(tgtssh, "app", port).get("default-prio") == ["0"])
+ assert dcb_tokens(tgtssh, "apptrust", port).get("order") == ["pcp"]
+ assert not dcb_tokens(tgtssh, "rewr", port).get("prio-dscp")
+ else:
+ until(lambda: [r for r in flower_rules(tgtssh, port) if not r[1]] == [(900, {}, 0)])
+ assert not tgtssh.runsh(f"tc filter show dev {port} egress").stdout.strip()
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_ingress_classify/topology.dot b/test/case/interfaces/qos_ingress_classify/topology.dot
new file mode 120000
index 0000000000..4f53d15af5
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x2.dot
\ No newline at end of file
diff --git a/test/case/interfaces/qos_ingress_classify/topology.svg b/test/case/interfaces/qos_ingress_classify/topology.svg
new file mode 100644
index 0000000000..ff3d246be6
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/topology.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_mixed_selection/Readme.adoc b/test/case/interfaces/qos_mixed_selection/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_mixed_selection/test.adoc b/test/case/interfaces/qos_mixed_selection/test.adoc
new file mode 100644
index 0000000000..4d1324fcbd
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/test.adoc
@@ -0,0 +1,38 @@
+=== QoS Mixed Selection
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_mixed_selection]
+
+==== Description
+
+Three talkers, marked EF, AF21 and CS1, bridged through the DUT and out
+one egress port that cannot carry them all. The port is negotiated down
+to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where
+it has no PHY. The ingress port classifies EF to priority 4, AF21 to 3
+and CS1 to 2, classes 4, 3 and 2 in the default table: EF in a strict
+class above the two weighted ones, which share 60 and 30 percent, with
+the two classes below holding five percent each and carrying nothing.
+
+EF offers 40 percent of what the port drains, the two weighted talkers
+60 percent each, so the port is oversubscribed and both weighted classes
+always have a backlog. EF must arrive intact, and the two weighted
+flows must divide what EF leaves 2:1, within five points, both of them
+losing more than a tenth of their frames. The test's own iperf3 control
+traffic, DSCP 0, is classified to priority 7. Skipped on a switch
+fabric whose scheduler is not offloaded, where forwarded frames never
+meet the configured algorithm.
+
+==== Topology
+
+image::topology.svg[QoS Mixed Selection topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Bridge the two ports, classify by DSCP on ingress, EF strict above AF21 and CS1 at 2:1
+. Slow the egress port so its queues can fill
+. Set up the talker and listener namespaces
+. EF at 40 percent, AF21 and CS1 at 60 percent each
+. Verify EF arrived intact
+. Verify AF21 and CS1 both lost frames and split the rest 2:1
+
+
diff --git a/test/case/interfaces/qos_mixed_selection/test.py b/test/case/interfaces/qos_mixed_selection/test.py
new file mode 100755
index 0000000000..4a6971e5b7
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/test.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+"""
+QoS Mixed Selection
+
+Three talkers, marked EF, AF21 and CS1, bridged through the DUT and out
+one egress port that cannot carry them all. The port is negotiated down
+to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where
+it has no PHY. The ingress port classifies EF to priority 4, AF21 to 3
+and CS1 to 2, classes 4, 3 and 2 in the default table: EF in a strict
+class above the two weighted ones, which share 60 and 30 percent, with
+the two classes below holding five percent each and carrying nothing.
+
+EF offers 40 percent of what the port drains, the two weighted talkers
+60 percent each, so the port is oversubscribed and both weighted classes
+always have a backlog. EF must arrive intact, and the two weighted
+flows must divide what EF leaves 2:1, within five points, both of them
+losing more than a tenth of their frames. The test's own iperf3 control
+traffic, DSCP 0, is classified to priority 7. Skipped on a switch
+fabric whose scheduler is not offloaded, where forwarded frames never
+meet the configured algorithm.
+"""
+import infamy
+import infamy.qos as qos
+from infamy.util import until
+
+EF, AF21, CS1 = 46 << 2, 18 << 2, 8 << 2 # TOS bytes of the three markings
+STRICT, HIGH, LOW = 4, 3, 2 # priorities, and classes, of the markings
+FILLER = 5 # share of each class below, carrying nothing
+
+SHARES = {HIGH: 60, LOW: 30}
+SHARES.update({tc: FILLER for tc in range(LOW)})
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ port = infamy.capability.Port(target, td1, tgtssh)
+ num_tc = port.traffic_classes
+ print(f"{td1}: {num_tc} traffic classes")
+ port.require_classes(test, 4)
+ port.require_offload(test, "transmission-selection")
+
+ with test.step("Bridge the two ports, classify by DSCP on ingress, EF strict above AF21 and CS1 at 2:1"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": "br0", "type": "infix-if-type:bridge", "enabled": True},
+ {
+ "name": td0,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "ingress": {"trust": "dscp",
+ "dscp-map": qos.dscp_map(**{"0": 7, "46": STRICT,
+ "18": HIGH, "8": LOW})}
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "egress": {"traffic-class": qos.traffic_classes(SHARES)}
+ }
+ },
+ ]
+ }
+ }})
+ quanta = [SHARES[tc] * qos.ETS_QUANTUM_UNIT for tc in sorted(SHARES, reverse=True)]
+ until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc,
+ qos.TABLE_8_5[num_tc],
+ strict=num_tc - len(SHARES), quanta=quanta))
+ qos.show_offload(port)
+
+ with test.step("Slow the egress port so its queues can fill"):
+ drain = qos.slow_port(port, until)
+ if not drain:
+ print(f"{td1} can neither negotiate down nor be rate limited, skipping")
+ test.skip()
+ print(f"{td1} drains {drain // 1_000_000} Mbit/s")
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up the talker and listener namespaces"):
+ ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface")
+ ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface")
+ ns0.must_reach("192.168.20.2")
+
+ with test.step("EF at 40 percent, AF21 and CS1 at 60 percent each"):
+ flows = [qos.Flow("EF", 5201, EF, int(drain * 0.4)),
+ qos.Flow("AF21", 5202, AF21, int(drain * 0.6)),
+ qos.Flow("CS1", 5203, CS1, int(drain * 0.6))]
+ total = qos.run_flows(ns0, ns1, "192.168.20.2", flows)
+ ef, af21, cs1 = flows
+ print(f"shares: EF {ef.share(total):.1f}%, AF21 {af21.share(total):.1f}%, "
+ f"CS1 {cs1.share(total):.1f}%")
+
+ with test.step("Verify EF arrived intact"):
+ assert ef.result["lost_percent"] < 1, \
+ f"EF lost {ef.result['lost']} of {ef.result['offered']} datagrams"
+
+ with test.step("Verify AF21 and CS1 both lost frames and split the rest 2:1"):
+ for flow in (af21, cs1):
+ assert flow.result["lost_percent"] > 10, \
+ f"{flow.name} lost {flow.result['lost_percent']:.1f}%, the port was never full"
+ rest = af21.result["packets"] + cs1.result["packets"]
+ af21_share = af21.share(rest)
+ print(f"of what EF left: AF21 {af21_share:.1f}%, CS1 {100 - af21_share:.1f}%")
+ assert abs(af21_share - 100 * 60 / 90) < 5, \
+ f"AF21 got {af21_share:.1f}% of what EF left, configured 2:1"
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_mixed_selection/topology.dot b/test/case/interfaces/qos_mixed_selection/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_mixed_selection/topology.svg b/test/case/interfaces/qos_mixed_selection/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_queue_assignment/Readme.adoc b/test/case/interfaces/qos_queue_assignment/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_queue_assignment/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_queue_assignment/test.adoc b/test/case/interfaces/qos_queue_assignment/test.adoc
new file mode 100644
index 0000000000..5cc8eaf45d
--- /dev/null
+++ b/test/case/interfaces/qos_queue_assignment/test.adoc
@@ -0,0 +1,35 @@
+=== QoS Queue Assignment
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_queue_assignment]
+
+==== Description
+
+Verify that a frame is queued in the traffic class its priority maps to,
+for the ieee preset, the ieee-sr preset and a custom table. The
+existing tests check the tables as tc arguments; this one
+checks the queue a frame actually reached.
+
+Frames enter tagged on a VLAN interface whose ingress-qos takes the
+priority straight from the PCP, so classification contributes nothing
+and the traffic class table on the egress port is all that is under
+test. A hundred datagrams are sent at each priority, routed through the
+DUT to the listener, and the per-class counters of the egress scheduler
+are read before and after: the class the table names must grow by at
+least that many, and no other class by anywhere near it. The DUT's own
+chatter, mDNS and neighbour discovery at priority 0 and control frames
+at priority 7, is a handful of frames and stays well below the burst.
+No load, no congestion.
+
+==== Topology
+
+image::topology.svg[QoS Queue Assignment topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Configure a routed path, tagged ingress with priority from PCP, egress under test
+. Set up the talker on VLAN 10 and the listener, resolve neighbours
+. Apply the {name} traffic class table on the egress port
+. Send {COUNT} frames at each priority, verify the class each reached
+
+
diff --git a/test/case/interfaces/qos_queue_assignment/test.py b/test/case/interfaces/qos_queue_assignment/test.py
new file mode 100755
index 0000000000..516f6f4027
--- /dev/null
+++ b/test/case/interfaces/qos_queue_assignment/test.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""
+QoS Queue Assignment
+
+Verify that a frame is queued in the traffic class its priority maps to,
+for the ieee preset, the ieee-sr preset and a custom table. The
+existing tests check the tables as tc arguments; this one
+checks the queue a frame actually reached.
+
+Frames enter tagged on a VLAN interface whose ingress-qos takes the
+priority straight from the PCP, so classification contributes nothing
+and the traffic class table on the egress port is all that is under
+test. A hundred datagrams are sent at each priority, routed through the
+DUT to the listener, and the per-class counters of the egress scheduler
+are read before and after: the class the table names must grow by at
+least that many, and no other class by anywhere near it. The DUT's own
+chatter, mDNS and neighbour discovery at priority 0 and control frames
+at priority 7, is a handful of frames and stays well below the burst.
+No load, no congestion.
+"""
+import infamy
+import infamy.qos as qos
+from infamy.util import until
+
+PRIORITIES = range(8)
+COUNT = 100 # per priority; the DUT's own chatter is a few frames
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ num_tc = infamy.capability.Port(target, td0).traffic_classes
+ print(f"{td1}: {num_tc} traffic classes")
+ tables = {
+ "ieee": ({"preset": "ieee"}, qos.TABLE_8_5[num_tc]),
+ "ieee-sr": ({"preset": "ieee-sr"}, qos.TABLE_34_1[num_tc]),
+ }
+ # Table 8-5 with the two lowest classes swapped, custom on any class count
+ custom = [1 if tc == 0 else 0 if tc == 1 else tc for tc in qos.TABLE_8_5[num_tc]]
+ tables["custom"] = ({f"priority{p}": tc for p, tc in enumerate(custom)}, custom)
+
+ with test.step("Configure a routed path, tagged ingress with priority from PCP, egress under test"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": td0, "enabled": True},
+ {
+ "name": f"{td0}.10",
+ "type": "infix-if-type:vlan",
+ "vlan": {
+ "id": 10,
+ "lower-layer-if": td0,
+ "ingress-qos": {"priority": "from-pcp"},
+ },
+ "ipv4": {
+ "forwarding": True,
+ "address": [{"ip": "192.168.10.1", "prefix-length": 24}]
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{"ip": "192.168.11.1", "prefix-length": 24}]
+ }
+ },
+ ]
+ }
+ }})
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up the talker on VLAN 10 and the listener, resolve neighbours"):
+ ns0.runsh("""
+ set -ex
+ ip link set iface up
+ ip link add dev vlan10 link iface up type vlan id 10
+ ip addr add 192.168.10.2/24 dev vlan10
+ """)
+ ns0.addroute("default", "192.168.10.1")
+ ns1.runsh("""
+ set -ex
+ ip link set iface up
+ ip addr add 192.168.11.2/24 dev iface
+ """)
+ ns1.addroute("default", "192.168.11.1")
+ ns0.must_reach("192.168.11.2")
+ dut_mac = qos.neighbour_mac(ns0, "192.168.10.1")
+ assert dut_mac, "DUT MAC not resolved"
+ print(f"DUT {td0}.10 is {dut_mac}")
+
+ for name, (table, prio_map) in tables.items():
+ with test.step(f"Apply the {name} traffic class table on the egress port"):
+ # Preset and custom leaves are a choice, so clear the previous
+ # table; the first round has none to clear
+ try:
+ target.delete_xpath(qos.xpath(td1, "/egress/traffic-class-table"))
+ except ValueError:
+ pass
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": td1,
+ "infix-interfaces:qos": {"egress": {"traffic-class-table": table}}
+ }]
+ }
+ }})
+ until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc, prio_map))
+ ns0.must_reach("192.168.11.2")
+
+ with test.step(f"Send {COUNT} frames at each priority, verify the class each reached"):
+ for prio in PRIORITIES:
+ before = qos.class_stats(tgtssh, td1, num_tc)
+ qos.mausezahn(ns0, "iface", "192.168.10.2", "192.168.11.2", dut_mac,
+ count=COUNT, vid=10, pcp=prio)
+
+ def landed():
+ delta = qos.stats_delta(before, qos.class_stats(tgtssh, td1, num_tc))
+ return delta.get(prio_map[prio], {}).get("packets", 0) >= COUNT
+
+ until(landed, attempts=20)
+ delta = qos.stats_delta(before, qos.class_stats(tgtssh, td1, num_tc))
+ got = {tc: d["packets"] for tc, d in delta.items() if d["packets"]}
+ print(f"{name}: priority {prio} -> class {prio_map[prio]}: {got}")
+
+ for tc, d in delta.items():
+ if tc == prio_map[prio]:
+ assert d["packets"] >= COUNT, \
+ f"priority {prio}: class {tc} got {d['packets']}, expected at least {COUNT}"
+ else:
+ assert d["packets"] < COUNT // 2, \
+ f"priority {prio}: class {tc} got {d['packets']} frames it should not have"
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_queue_assignment/topology.dot b/test/case/interfaces/qos_queue_assignment/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_queue_assignment/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_queue_assignment/topology.svg b/test/case/interfaces/qos_queue_assignment/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_queue_assignment/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_rate_limit/Readme.adoc b/test/case/interfaces/qos_rate_limit/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_rate_limit/test.adoc b/test/case/interfaces/qos_rate_limit/test.adoc
new file mode 100644
index 0000000000..2d9b4ebec8
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/test.adoc
@@ -0,0 +1,33 @@
+=== QoS Rate Limit
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_rate_limit]
+
+==== Description
+
+One talker, bridged through the DUT and out an egress port with a
+10 Mbit/s rate limit, offering three times that. What arrives at the
+listener must be the limit, within a fifth, whatever the priority of the
+flow: once marked EF, in a strict class near the top, once marked CS1,
+in the lowest class. Then both at once, each offering one and a half
+times the limit, must still add up to the limit.
+
+A limit that lets more through is not a limit, and a limit that only
+bites on some queues is a scheduler fault dressed up as one. The test's
+own iperf3 control traffic, DSCP 0, is classified to priority 7 at the
+ingress port. Skipped on a switch fabric whose driver does not offload
+the rate limit, where forwarded frames never meet the bucket.
+
+==== Topology
+
+image::topology.svg[QoS Rate Limit topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Bridge the two ports, trust DSCP on ingress, rate limit the egress port
+. Set up the talker and listener namespaces
+. One EF flow at three times the limit, expect the limit through
+. One CS1 flow at three times the limit, expect the limit through
+. Both at one and a half times the limit, expect the limit through in total
+
+
diff --git a/test/case/interfaces/qos_rate_limit/test.py b/test/case/interfaces/qos_rate_limit/test.py
new file mode 100755
index 0000000000..6218b25405
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/test.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""
+QoS Rate Limit
+
+One talker, bridged through the DUT and out an egress port with a
+10 Mbit/s rate limit, offering three times that. What arrives at the
+listener must be the limit, within a fifth, whatever the priority of the
+flow: once marked EF, in a strict class near the top, once marked CS1,
+in the lowest class. Then both at once, each offering one and a half
+times the limit, must still add up to the limit.
+
+A limit that lets more through is not a limit, and a limit that only
+bites on some queues is a scheduler fault dressed up as one. The test's
+own iperf3 control traffic, DSCP 0, is classified to priority 7 at the
+ingress port. Skipped on a switch fabric whose driver does not offload
+the rate limit, where forwarded frames never meet the bucket.
+"""
+import infamy
+import infamy.qos as qos
+from infamy.util import until
+
+LIMIT = 10_000_000 # bit/s, Layer 2
+EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings
+SIZE = 1000 # UDP payload, 1042 bytes on the wire
+TOLERANCE = 0.2
+
+
+def check(what, throughput):
+ """throughput is payload bit/s; the limit counts the 42 header bytes too"""
+ expect = LIMIT * SIZE / (SIZE + 42)
+ print(f"{what}: {throughput / 1e6:.2f} Mbit/s through, limit lets {expect / 1e6:.2f}")
+ assert abs(throughput - expect) <= TOLERANCE * expect, \
+ f"{what}: {throughput / 1e6:.2f} Mbit/s through a {LIMIT / 1e6:.0f} Mbit/s limit"
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+ port = infamy.capability.Port(target, td1, tgtssh)
+
+ with test.step("Bridge the two ports, trust DSCP on ingress, rate limit the egress port"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": "br0", "type": "infix-if-type:bridge", "enabled": True},
+ {
+ "name": td0,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "ingress": {"trust": "dscp",
+ "dscp-map": qos.dscp_map(**{"0": 7, "46": 5, "8": 1})}
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "egress": {"rate-limit": {"rate": LIMIT}}
+ }
+ },
+ ]
+ }
+ }})
+ until(lambda: (qos.root_qdisc(tgtssh, td1) or {}).get("kind") == "tbf")
+ qos.show_shaper(port)
+
+ port.require_offload(test, "rate-limit")
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up the talker and listener namespaces"):
+ ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface")
+ ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface")
+ ns0.must_reach("192.168.20.2")
+
+ with test.step("One EF flow at three times the limit, expect the limit through"):
+ flow = qos.Flow("EF", 5201, EF, 3 * LIMIT, size=SIZE)
+ qos.run_flows(ns0, ns1, "192.168.20.2", [flow])
+ check("EF alone", flow.throughput())
+
+ with test.step("One CS1 flow at three times the limit, expect the limit through"):
+ flow = qos.Flow("CS1", 5202, CS1, 3 * LIMIT, size=SIZE)
+ qos.run_flows(ns0, ns1, "192.168.20.2", [flow])
+ check("CS1 alone", flow.throughput())
+
+ with test.step("Both at one and a half times the limit, expect the limit through in total"):
+ flows = [qos.Flow("EF", 5201, EF, int(1.5 * LIMIT), size=SIZE),
+ qos.Flow("CS1", 5202, CS1, int(1.5 * LIMIT), size=SIZE)]
+ total = qos.run_flows(ns0, ns1, "192.168.20.2", flows)
+ print(f"shares: EF {flows[0].share(total):.1f}%, CS1 {flows[1].share(total):.1f}%")
+ check("EF and CS1 together", sum(f.throughput() for f in flows))
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_rate_limit/topology.dot b/test/case/interfaces/qos_rate_limit/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_rate_limit/topology.svg b/test/case/interfaces/qos_rate_limit/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_strict_priority/Readme.adoc b/test/case/interfaces/qos_strict_priority/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_strict_priority/test.adoc b/test/case/interfaces/qos_strict_priority/test.adoc
new file mode 100644
index 0000000000..bd2cfff3f7
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/test.adoc
@@ -0,0 +1,48 @@
+=== QoS Strict Priority
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_strict_priority]
+
+==== Description
+
+Two talkers, one marked EF and one CS1, bridged through the DUT and out
+one egress port that cannot carry both. The port is negotiated down to
+100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where it
+has no PHY, and each talker offers 80 percent of what the port drains,
+so the queues fill and the scheduler decides who gets through.
+
+Classification at the ingress port decides the priority, and with it
+the queue, all the way to the listener: on one chip, across a cascade,
+or through a network of switches. So the flows are steered with the
+DSCP map on the ingress port and the class table is left at its
+default. Three measurements, in this order:
+
+ - baseline: both markings classified to the same priority share one
+ queue, and the low flow keeps at least a tenth of it; one FIFO under
+ tail drop is not fair, but it favours nobody by marking
+ - split: EF classified to priority 5, strict above CS1 at priority 2,
+ arrives without loss and CS1 takes what is left
+ - starve: EF raised to 120 percent of the port on its own leaves CS1
+ below 5 percent of the served datagrams
+
+The low flow must lose more than a tenth of its frames in every
+measurement; a port that was never full proves nothing. The test's own
+iperf3 control traffic, DSCP 0, is classified to priority 7 so the
+contest never cuts it off. Skipped on a switch fabric whose scheduler
+is not offloaded, where forwarded frames never meet the configured
+algorithm.
+
+==== Topology
+
+image::topology.svg[QoS Strict Priority topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Bridge the two ports, classify by DSCP on ingress, control traffic on top
+. Slow the egress port so its queues can fill
+. Set up the talker and listener namespaces
+. Baseline: both flows at one priority, 80 percent each, expect neither starved
+. Split: EF strict above CS1, 80 percent each, expect EF without loss
+. Starve: EF at 120 percent of the port, expect CS1 below 5 percent
+
+
diff --git a/test/case/interfaces/qos_strict_priority/test.py b/test/case/interfaces/qos_strict_priority/test.py
new file mode 100755
index 0000000000..8639a7e0b2
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/test.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""
+QoS Strict Priority
+
+Two talkers, one marked EF and one CS1, bridged through the DUT and out
+one egress port that cannot carry both. The port is negotiated down to
+100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where it
+has no PHY, and each talker offers 80 percent of what the port drains,
+so the queues fill and the scheduler decides who gets through.
+
+Classification at the ingress port decides the priority, and with it
+the queue, all the way to the listener: on one chip, across a cascade,
+or through a network of switches. So the flows are steered with the
+DSCP map on the ingress port and the class table is left at its
+default. Three measurements, in this order:
+
+ - baseline: both markings classified to the same priority share one
+ queue, and the low flow keeps at least a tenth of it; one FIFO under
+ tail drop is not fair, but it favours nobody by marking
+ - split: EF classified to priority 5, strict above CS1 at priority 2,
+ arrives without loss and CS1 takes what is left
+ - starve: EF raised to 120 percent of the port on its own leaves CS1
+ below 5 percent of the served datagrams
+
+The low flow must lose more than a tenth of its frames in every
+measurement; a port that was never full proves nothing. The test's own
+iperf3 control traffic, DSCP 0, is classified to priority 7 so the
+contest never cuts it off. Skipped on a switch fabric whose scheduler
+is not offloaded, where forwarded frames never meet the configured
+algorithm.
+"""
+import infamy
+import infamy.qos as qos
+from infamy.util import until
+
+EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings
+HIGH, LOW = 5, 2 # priorities the two markings are classified to
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ port = infamy.capability.Port(target, td1, tgtssh)
+ print(f"{td1}: {port.traffic_classes} traffic classes, "
+ f"EF at priority {HIGH}, CS1 at priority {LOW}")
+ port.require_offload(test, "transmission-selection")
+
+ with test.step("Bridge the two ports, classify by DSCP on ingress, control traffic on top"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": "br0", "type": "infix-if-type:bridge", "enabled": True},
+ {
+ "name": td0,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "ingress": {"trust": "dscp",
+ "dscp-map": qos.dscp_map(**{"0": 7, "46": HIGH, "8": LOW})}
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ },
+ ]
+ }
+ }})
+
+ with test.step("Slow the egress port so its queues can fill"):
+ drain = qos.slow_port(port, until)
+ if not drain:
+ print(f"{td1} can neither negotiate down nor be rate limited, skipping")
+ test.skip()
+ print(f"{td1} drains {drain // 1_000_000} Mbit/s")
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up the talker and listener namespaces"):
+ ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface")
+ ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface")
+ ns0.must_reach("192.168.20.2")
+
+ def apply(ef_prio, cs1_prio):
+ """Classify the two markings, then let the setup settle"""
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": td0,
+ "infix-interfaces:qos": {
+ "ingress": {"dscp-map": qos.dscp_map(**{"0": 7, "46": ef_prio,
+ "8": cs1_prio})}
+ }
+ }]
+ }
+ }})
+ until(lambda: qos.dscp_prio(tgtssh, td0, 46) == ef_prio and
+ qos.dscp_prio(tgtssh, td0, 8) == cs1_prio)
+ qos.show_offload(port)
+ ns0.must_reach("192.168.20.2")
+
+ def measure(high_rate, low_rate=0.8):
+ flows = [qos.Flow("EF", 5201, EF, int(drain * high_rate)),
+ qos.Flow("CS1", 5202, CS1, int(drain * low_rate))]
+ total = qos.run_flows(ns0, ns1, "192.168.20.2", flows)
+ high, low = flows
+ print(f"shares: EF {high.share(total):.1f}%, CS1 {low.share(total):.1f}%")
+ assert low.result["lost_percent"] > 10, \
+ f"the low priority flow lost {low.result['lost_percent']:.1f}%, the port was never full"
+ return high, low, total
+
+ with test.step("Baseline: both flows at one priority, 80 percent each, expect neither starved"):
+ apply(LOW, LOW)
+ high, low, total = measure(0.8)
+ assert low.share(total) >= 10, f"CS1 got {low.share(total):.1f}% with no priority in play"
+
+ with test.step("Split: EF strict above CS1, 80 percent each, expect EF without loss"):
+ apply(HIGH, LOW)
+ high, low, total = measure(0.8)
+ # EF offers less than the port drains, so it passes intact; a
+ # stray datagram at the iperf3 startup edge is not congestion
+ assert high.result["lost_percent"] < 1, \
+ f"EF lost {high.result['lost']} of {high.result['offered']} datagrams"
+
+ with test.step("Starve: EF at 120 percent of the port, expect CS1 below 5 percent"):
+ high, low, total = measure(1.2)
+ assert low.share(total) < 5, f"CS1 got {low.share(total):.1f}% of the port"
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_strict_priority/topology.dot b/test/case/interfaces/qos_strict_priority/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_strict_priority/topology.svg b/test/case/interfaces/qos_strict_priority/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_traffic_class/Readme.adoc b/test/case/interfaces/qos_traffic_class/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_traffic_class/test.adoc b/test/case/interfaces/qos_traffic_class/test.adoc
new file mode 100644
index 0000000000..0befc650b8
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/test.adoc
@@ -0,0 +1,38 @@
+=== QoS Traffic Classes and Transmission Selection
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_traffic_class]
+
+==== Description
+
+Configure a custom priority to traffic class map on a port with the two
+lowest classes sharing 67:33 and the rest strict priority, and verify the
+root qdisc reflects it: an ets qdisc with the strict bands first, the
+shares as quanta of one frame per percent, and the priomap inverted as
+802.1Q numbering requires. A driver that offloads it marks the qdisc
+offloaded.
+
+The port's class count comes from its transmit queues, or eight for a
+single-queue port. Switching the table to the ieee-sr preset must put
+the SR classes, priorities 2 and 3, on the two highest classes. A rate
+limit must put a tbf root qdisc above the scheduler, with the same ets
+table beneath it, and go away with the setting. Removing the qos
+container must restore the default table, IEEE 802.1Q-2022 Table 8-5,
+and the operational datastore must report whether transmission
+selection is offloaded throughout.
+
+==== Topology
+
+image::topology.svg[QoS Traffic Classes and Transmission Selection topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Configure a custom map, the two lowest classes sharing 67:33
+. Verify the root qdisc carries the custom map and shares
+. Verify the offload capability matches the qdisc
+. Switch to the ieee-sr preset and verify SR classes on top
+. Add a 10 Mbit/s rate limit and verify the tbf root above the ets table
+. Remove the rate limit and verify the scheduler is the root again
+. Remove qos configuration and verify the default table is back
+
+
diff --git a/test/case/interfaces/qos_traffic_class/test.py b/test/case/interfaces/qos_traffic_class/test.py
new file mode 100755
index 0000000000..64853c8164
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/test.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+"""
+QoS Traffic Classes and Transmission Selection
+
+Configure a custom priority to traffic class map on a port with the two
+lowest classes sharing 67:33 and the rest strict priority, and verify the
+root qdisc reflects it: an ets qdisc with the strict bands first, the
+shares as quanta of one frame per percent, and the priomap inverted as
+802.1Q numbering requires. A driver that offloads it marks the qdisc
+offloaded.
+
+The port's class count comes from its transmit queues, or eight for a
+single-queue port. Switching the table to the ieee-sr preset must put
+the SR classes, priorities 2 and 3, on the two highest classes. A rate
+limit must put a tbf root qdisc above the scheduler, with the same ets
+table beneath it, and go away with the setting. Removing the qos
+container must restore the default table, IEEE 802.1Q-2022 Table 8-5,
+and the operational datastore must report whether transmission
+selection is offloaded throughout.
+"""
+import json
+import infamy
+from infamy.util import until
+
+# IEEE 802.1Q-2022 Table 8-5 and Table 34-1, indexed by class count
+TABLE_8_5 = {
+ 2: [0, 0, 0, 0, 1, 1, 1, 1],
+ 3: [0, 0, 0, 0, 1, 1, 2, 2],
+ 4: [0, 0, 1, 1, 2, 2, 3, 3],
+ 5: [0, 0, 1, 1, 2, 2, 3, 4],
+ 6: [1, 0, 2, 2, 3, 3, 4, 5],
+ 7: [1, 0, 2, 3, 4, 4, 5, 6],
+ 8: [1, 0, 2, 3, 4, 5, 6, 7],
+}
+TABLE_34_1 = {
+ 2: [0, 0, 1, 1, 0, 0, 0, 0],
+ 3: [0, 0, 1, 2, 0, 0, 0, 0],
+ 4: [0, 0, 2, 3, 1, 1, 1, 1],
+ 5: [0, 0, 3, 4, 1, 1, 2, 2],
+ 6: [0, 0, 4, 5, 1, 1, 2, 3],
+ 7: [0, 0, 5, 6, 1, 2, 3, 4],
+ 8: [1, 0, 6, 7, 2, 3, 4, 5],
+}
+
+
+def qos_xpath(port, path=""):
+ return f"/ietf-interfaces:interfaces/interface[name='{port}']/infix-interfaces:qos{path}"
+
+
+def root_qdisc(ssh, port):
+ """Return the root qdisc of port as a dict, or None"""
+ out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout
+ for qdisc in json.loads(out or "[]"):
+ if qdisc.get("root"):
+ return qdisc
+ return None
+
+
+def qdisc_matches(qdisc, num_tc, prio_map, strict, quanta):
+ """Check a root qdisc against the expected 802.1Q table"""
+ if not qdisc:
+ return False
+ opts = qdisc.get("options", {})
+ if qdisc["kind"] == "ets":
+ return (opts.get("bands") == num_tc and opts.get("strict") == strict
+ and opts.get("quanta", []) == quanta
+ and opts.get("priomap", [])[:8] == [num_tc - 1 - tc for tc in prio_map])
+ return False
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, port = env.ltop.xlate("target", "data")
+
+ cap = infamy.capability.Port(target, port, tgtssh)
+ num_tc = cap.traffic_classes
+ print(f"{port}: {num_tc} traffic classes")
+ assert num_tc and 2 <= num_tc <= 8, f"max-traffic-classes {num_tc}"
+ offloaded = cap.offloads("transmission-selection")
+
+ with test.step("Configure a custom map, the two lowest classes sharing 67:33"):
+ # Table 8-5 with the two lowest classes swapped, so the map is
+ # visibly custom on any class count
+ custom = [1 if tc == 0 else 0 if tc == 1 else tc for tc in TABLE_8_5[num_tc]]
+ table = {f"priority{prio}": tc for prio, tc in enumerate(custom)}
+
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "enabled": True,
+ "infix-interfaces:qos": {
+ "egress": {
+ "traffic-class-table": table,
+ "traffic-class": [
+ {"id": 1, "algorithm": "ieee802-dot1q-types:enhanced-transmission-selection",
+ "bandwidth": 67},
+ {"id": 0, "algorithm": "ieee802-dot1q-types:enhanced-transmission-selection",
+ "bandwidth": 33},
+ ]
+ }
+ }
+ }]
+ }
+ }})
+
+ with test.step("Verify the root qdisc carries the custom map and shares"):
+ until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, custom, num_tc - 2,
+ [67 * 1514, 33 * 1514]))
+ print(json.dumps(root_qdisc(tgtssh, port)))
+
+ with test.step("Verify the offload capability matches the qdisc"):
+ taken = root_qdisc(tgtssh, port).get("offloaded", False)
+ until(lambda: cap.offloads("transmission-selection") == taken)
+
+ with test.step("Switch to the ieee-sr preset and verify SR classes on top"):
+ target.delete_xpath(qos_xpath(port, "/egress"))
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "infix-interfaces:qos": {
+ "egress": {"traffic-class-table": {"preset": "ieee-sr"}}
+ }
+ }]
+ }
+ }})
+ until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_34_1[num_tc], num_tc, []))
+
+ with test.step("Add a 10 Mbit/s rate limit and verify the tbf root above the ets table"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "infix-interfaces:qos": {
+ "egress": {"rate-limit": {"rate": 10000000}}
+ }
+ }]
+ }
+ }})
+
+ def rate_limited():
+ out = tgtssh.runsh(f"tc -j qdisc show dev {port}").stdout
+ qdiscs = json.loads(out or "[]")
+ root = [q for q in qdiscs if q.get("root")]
+ child = [q for q in qdiscs if q.get("parent") == "1:1"]
+ return (root and root[0]["kind"] == "tbf" and root[0]["options"]["rate"] == 1250000
+ and child and qdisc_matches(child[0], num_tc, TABLE_34_1[num_tc], num_tc, []))
+ until(rate_limited)
+ print(tgtssh.runsh(f"tc -j qdisc show dev {port}").stdout)
+
+ with test.step("Remove the rate limit and verify the scheduler is the root again"):
+ target.delete_xpath(qos_xpath(port, "/egress/rate-limit"))
+ until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_34_1[num_tc], num_tc, []))
+
+ with test.step("Remove qos configuration and verify the default table is back"):
+ target.delete_xpath(qos_xpath(port))
+ until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_8_5[num_tc], num_tc, []))
+ assert cap.offloads("transmission-selection") == offloaded
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_traffic_class/topology.dot b/test/case/interfaces/qos_traffic_class/topology.dot
new file mode 120000
index 0000000000..4f53d15af5
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x2.dot
\ No newline at end of file
diff --git a/test/case/interfaces/qos_traffic_class/topology.svg b/test/case/interfaces/qos_traffic_class/topology.svg
new file mode 100644
index 0000000000..ff3d246be6
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/topology.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_validation/Readme.adoc b/test/case/interfaces/qos_validation/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_validation/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_validation/test.adoc b/test/case/interfaces/qos_validation/test.adoc
new file mode 100644
index 0000000000..d031810eee
--- /dev/null
+++ b/test/case/interfaces/qos_validation/test.adoc
@@ -0,0 +1,34 @@
+=== QoS Configuration Validation
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_validation]
+
+==== Description
+
+Verify that traffic class layouts the device cannot express are rejected
+at commit time, with the running configuration left untouched:
+
+ - a strict-priority class below a weighted class
+ - weighted shares that do not sum to 100 percent
+ - a transmission selection algorithm outside strict-priority and
+ enhanced-transmission-selection
+ - a traffic class beyond the port's class count
+ - a priority mapped to a class beyond the port's class count
+
+The last two apply on ports with fewer than eight classes. A valid
+layout must still be accepted afterwards.
+
+==== Topology
+
+image::topology.svg[QoS Configuration Validation topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Reject strict-priority class below a weighted class
+. Reject weighted shares that do not sum to 100
+. Reject credit-based-shaper algorithm
+. Reject traffic class beyond the port's class count
+. Reject priority mapped beyond the port's class count
+. Accept a valid layout with strict classes above weighted
+
+
diff --git a/test/case/interfaces/qos_validation/test.py b/test/case/interfaces/qos_validation/test.py
new file mode 100755
index 0000000000..c39a655a8e
--- /dev/null
+++ b/test/case/interfaces/qos_validation/test.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""
+QoS Configuration Validation
+
+Verify that traffic class layouts the device cannot express are rejected
+at commit time, with the running configuration left untouched:
+
+ - a strict-priority class below a weighted class
+ - weighted shares that do not sum to 100 percent
+ - a transmission selection algorithm outside strict-priority and
+ enhanced-transmission-selection
+ - a traffic class beyond the port's class count
+ - a priority mapped to a class beyond the port's class count
+
+The last two apply on ports with fewer than eight classes. A valid
+layout must still be accepted afterwards.
+"""
+import infamy
+
+STRICT = "ieee802-dot1q-types:strict-priority"
+ETS = "ieee802-dot1q-types:enhanced-transmission-selection"
+CBS = "ieee802-dot1q-types:credit-based-shaper"
+
+
+def egress_config(port, egress):
+ return {"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "enabled": True,
+ "infix-interfaces:qos": {"egress": egress}
+ }]
+ }
+ }}
+
+
+def must_reject(target, port, egress, what, *reasons):
+ """The commit must fail, and the error must name one of the reasons"""
+ try:
+ target.put_config_dicts(egress_config(port, egress))
+ except Exception as err:
+ text = getattr(getattr(err, "response", None), "text", None) or str(err)
+ matched = [reason for reason in reasons if reason in text]
+ assert matched, f"{what} rejected for another reason:\n{text}"
+ print(f"Rejected as expected: {matched[0]}")
+ return
+ raise AssertionError(f"{what} was accepted")
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ _, port = env.ltop.xlate("target", "data")
+ num_tc = infamy.capability.Port(target, port).traffic_classes
+ print(f"{port}: {num_tc} traffic classes")
+ assert num_tc and num_tc >= 2, f"max-traffic-classes {num_tc}"
+ top = num_tc - 1
+
+ with test.step("Reject strict-priority class below a weighted class"):
+ must_reject(target, port, {
+ "traffic-class": [
+ {"id": top, "algorithm": ETS, "bandwidth": 100},
+ {"id": top - 1, "algorithm": STRICT},
+ ]
+ }, "strict class below weighted class",
+ "strict-priority classes must be the highest-numbered")
+
+ with test.step("Reject weighted shares that do not sum to 100"):
+ # A must in the model, not the daemon: NETCONF relays the model's
+ # message, the RESTCONF error body only says validation failed
+ must_reject(target, port, {
+ "traffic-class": [
+ {"id": 1, "algorithm": ETS, "bandwidth": 60},
+ {"id": 0, "algorithm": ETS, "bandwidth": 30},
+ ]
+ }, "shares summing to 90", "must sum to 100", "Validation failed")
+
+ with test.step("Reject credit-based-shaper algorithm"):
+ must_reject(target, port, {
+ "traffic-class": [{"id": top, "algorithm": CBS}]
+ }, "credit-based-shaper", "not supported, use strict-priority")
+
+ if num_tc < 8:
+ with test.step("Reject traffic class beyond the port's class count"):
+ must_reject(target, port, {
+ "traffic-class": [{"id": num_tc}]
+ }, f"traffic class {num_tc} on a {num_tc} class port",
+ f"traffic class {num_tc}, port has {num_tc} classes")
+
+ with test.step("Reject priority mapped beyond the port's class count"):
+ must_reject(target, port, {
+ "traffic-class-table": {"priority7": num_tc},
+ }, f"priority7 mapped to class {num_tc} on a {num_tc} class port",
+ f"priority7 maps to traffic class {num_tc}, port has {num_tc} classes")
+
+ with test.step("Accept a valid layout with strict classes above weighted"):
+ target.put_config_dicts(egress_config(port, {
+ "traffic-class": [
+ {"id": top, "algorithm": STRICT},
+ {"id": 1, "algorithm": ETS, "bandwidth": 67},
+ {"id": 0, "algorithm": ETS, "bandwidth": 33},
+ ]
+ }))
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_validation/topology.dot b/test/case/interfaces/qos_validation/topology.dot
new file mode 120000
index 0000000000..4f53d15af5
--- /dev/null
+++ b/test/case/interfaces/qos_validation/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x2.dot
\ No newline at end of file
diff --git a/test/case/interfaces/qos_validation/topology.svg b/test/case/interfaces/qos_validation/topology.svg
new file mode 100644
index 0000000000..ff3d246be6
--- /dev/null
+++ b/test/case/interfaces/qos_validation/topology.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/test/case/interfaces/qos_weighted_sharing/Readme.adoc b/test/case/interfaces/qos_weighted_sharing/Readme.adoc
new file mode 120000
index 0000000000..ae32c84120
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/Readme.adoc
@@ -0,0 +1 @@
+test.adoc
\ No newline at end of file
diff --git a/test/case/interfaces/qos_weighted_sharing/test.adoc b/test/case/interfaces/qos_weighted_sharing/test.adoc
new file mode 100644
index 0000000000..1b0868609a
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/test.adoc
@@ -0,0 +1,46 @@
+=== QoS Weighted Sharing
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_weighted_sharing]
+
+==== Description
+
+Two talkers, one marked EF and one marked CS1, bridged through the DUT
+and out one egress port that cannot carry both. The port is negotiated
+down to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s
+where it has no PHY. The ingress port classifies EF to priority 3 and
+CS1 to priority 2, classes 3 and 2 in the default table, both running
+enhanced transmission selection; the two classes below hold five
+percent each and carry nothing, since strict classes must sit above the
+weighted ones. Each talker offers 80 percent of what the port drains,
+so both classes always have a backlog and the scheduler decides the
+split.
+
+Two measurements:
+
+ - equal weights, 45 and 45 percent, must give an even split. This is
+ the baseline, and fails on its own if the shares are applied to the
+ wrong bands
+ - 2:1, configured as 60 and 30 percent, must move the split to match
+
+Each flow's share of the delivered datagrams must land within five
+points of its configured share of the two, and both flows must lose more
+than a tenth of their frames in both measurements; a port that was never
+full proves nothing. The test's own iperf3 control traffic, DSCP 0, is
+classified to priority 7. Skipped on a switch fabric whose scheduler is
+not offloaded, where forwarded frames never meet the configured
+algorithm.
+
+==== Topology
+
+image::topology.svg[QoS Weighted Sharing topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUT
+. Bridge the two ports, classify by DSCP on ingress, EF and CS1 in two weighted classes
+. Slow the egress port so its queues can fill
+. Set up the talker and listener namespaces
+. Equal weights, both flows at 80 percent, expect an even split
+. Weights 2:1, both flows at 80 percent, expect a 67:33 split
+
+
diff --git a/test/case/interfaces/qos_weighted_sharing/test.py b/test/case/interfaces/qos_weighted_sharing/test.py
new file mode 100755
index 0000000000..eda3bdc285
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/test.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""
+QoS Weighted Sharing
+
+Two talkers, one marked EF and one marked CS1, bridged through the DUT
+and out one egress port that cannot carry both. The port is negotiated
+down to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s
+where it has no PHY. The ingress port classifies EF to priority 3 and
+CS1 to priority 2, classes 3 and 2 in the default table, both running
+enhanced transmission selection; the two classes below hold five
+percent each and carry nothing, since strict classes must sit above the
+weighted ones. Each talker offers 80 percent of what the port drains,
+so both classes always have a backlog and the scheduler decides the
+split.
+
+Two measurements:
+
+ - equal weights, 45 and 45 percent, must give an even split. This is
+ the baseline, and fails on its own if the shares are applied to the
+ wrong bands
+ - 2:1, configured as 60 and 30 percent, must move the split to match
+
+Each flow's share of the delivered datagrams must land within five
+points of its configured share of the two, and both flows must lose more
+than a tenth of their frames in both measurements; a port that was never
+full proves nothing. The test's own iperf3 control traffic, DSCP 0, is
+classified to priority 7. Skipped on a switch fabric whose scheduler is
+not offloaded, where forwarded frames never meet the configured
+algorithm.
+"""
+import infamy
+import infamy.qos as qos
+from infamy.util import until
+
+EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings
+HIGH, LOW = 3, 2 # priorities, and classes, of the two markings
+FILLER = 5 # share of each class below the two, carrying nothing
+
+
+def layout(high, low):
+ """The weighted classes: the two under test and the fillers below"""
+ shares = {HIGH: high, LOW: low}
+ shares.update({tc: FILLER for tc in range(LOW)})
+ return qos.traffic_classes(shares)
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ tgtssh = env.attach("target", "mgmt", "ssh")
+ _, td0 = env.ltop.xlate("target", "data1")
+ _, td1 = env.ltop.xlate("target", "data2")
+ _, hd0 = env.ltop.xlate("host", "data1")
+ _, hd1 = env.ltop.xlate("host", "data2")
+
+ port = infamy.capability.Port(target, td1, tgtssh)
+ num_tc = port.traffic_classes
+ print(f"{td1}: {num_tc} traffic classes")
+ port.require_classes(test, 3)
+ port.require_offload(test, "transmission-selection")
+
+ with test.step("Bridge the two ports, classify by DSCP on ingress, EF and CS1 in two weighted classes"):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {"name": "br0", "type": "infix-if-type:bridge", "enabled": True},
+ {
+ "name": td0,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "ingress": {"trust": "dscp",
+ "dscp-map": qos.dscp_map(**{"0": 7, "46": HIGH, "8": LOW})}
+ }
+ },
+ {
+ "name": td1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {"bridge": "br0"},
+ "infix-interfaces:qos": {
+ "egress": {"traffic-class": layout(45, 45)}
+ }
+ },
+ ]
+ }
+ }})
+
+ with test.step("Slow the egress port so its queues can fill"):
+ drain = qos.slow_port(port, until)
+ if not drain:
+ print(f"{td1} can neither negotiate down nor be rate limited, skipping")
+ test.skip()
+ print(f"{td1} drains {drain // 1_000_000} Mbit/s")
+
+ with infamy.IsolatedMacVlan(hd0) as ns0, \
+ infamy.IsolatedMacVlan(hd1) as ns1:
+
+ with test.step("Set up the talker and listener namespaces"):
+ ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface")
+ ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface")
+ ns0.must_reach("192.168.20.2")
+
+ def apply(high, low):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": td1,
+ "infix-interfaces:qos": {"egress": {"traffic-class": layout(high, low)}}
+ }]
+ }
+ }})
+ quanta = [share * qos.ETS_QUANTUM_UNIT
+ for share in [high, low] + [FILLER] * LOW]
+ until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc,
+ qos.TABLE_8_5[num_tc],
+ strict=num_tc - 2 - LOW, quanta=quanta))
+ qos.show_offload(port)
+ ns0.must_reach("192.168.20.2")
+
+ def measure(high, low):
+ apply(high, low)
+ flows = [qos.Flow("EF", 5201, EF, int(drain * 0.8)),
+ qos.Flow("CS1", 5202, CS1, int(drain * 0.8))]
+ total = qos.run_flows(ns0, ns1, "192.168.20.2", flows)
+ ef, cs1 = flows
+ expect = 100.0 * high / (high + low)
+ print(f"shares: EF {ef.share(total):.1f}%, CS1 {cs1.share(total):.1f}%, "
+ f"configured {high}:{low}")
+ for flow in flows:
+ assert flow.result["lost_percent"] > 10, \
+ f"{flow.name} lost {flow.result['lost_percent']:.1f}%, the port was never full"
+ assert abs(ef.share(total) - expect) < 5, \
+ f"EF got {ef.share(total):.1f}% of the port, configured {expect:.0f}%"
+
+ with test.step("Equal weights, both flows at 80 percent, expect an even split"):
+ measure(45, 45)
+
+ with test.step("Weights 2:1, both flows at 80 percent, expect a 67:33 split"):
+ measure(60, 30)
+
+ test.succeed()
diff --git a/test/case/interfaces/qos_weighted_sharing/topology.dot b/test/case/interfaces/qos_weighted_sharing/topology.dot
new file mode 100644
index 0000000000..f288f2f951
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}
diff --git a/test/case/interfaces/qos_weighted_sharing/topology.svg b/test/case/interfaces/qos_weighted_sharing/topology.svg
new file mode 100644
index 0000000000..9517e6397f
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/infamy/__init__.py b/test/infamy/__init__.py
index c3abc6f19f..e6ab297eff 100644
--- a/test/infamy/__init__.py
+++ b/test/infamy/__init__.py
@@ -1,5 +1,6 @@
import os
+from . import capability
from .container import Container
from .env import Env
from .env import ArgumentParser
diff --git a/test/infamy/capability.py b/test/infamy/capability.py
new file mode 100644
index 0000000000..2030ecef6a
--- /dev/null
+++ b/test/infamy/capability.py
@@ -0,0 +1,98 @@
+"""
+What a device under test can actually do.
+
+Tests ask here instead of working it out from sysfs and qdisc dumps
+themselves, so a rig whose fabric differs skips with a stated reason
+rather than failing a measurement that was never meaningful.
+"""
+
+
+class Port:
+ """What one interface of a device under test can do.
+
+ What the port is is read once, over ssh, since it cannot change
+ while a test runs. What the port is currently doing is read every
+ time, since the test is what changes it. An ssh transport is only
+ needed for the former.
+ """
+
+ def __init__(self, target, name, ssh=None):
+ self.target = target
+ self.name = name
+ self.ssh = ssh
+ self._switched = None
+ self._driver = None
+
+ @property
+ def caps(self):
+ """The qos capabilities the port reports, {} when it reports none"""
+ data = self.target.get_data(
+ f"/ietf-interfaces:interfaces/interface[name='{self.name}']"
+ "/infix-interfaces:qos/capabilities")
+ iface = next(iter(data["interfaces"]["interface"]), {})
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+
+ @property
+ def switched(self):
+ """Whether the port belongs to a switch fabric.
+
+ Such a port forwards most frames without the CPU seeing them, so
+ whatever the kernel alone would do to them does not happen. A
+ fabric port carries the chip's id in sysfs. DSA ports are asked
+ a second way because that id reaches them through devlink, which
+ has not been confirmed on every generation we run on.
+ """
+ if self._switched is None:
+ out = self.ssh.runsh(
+ f"cat /sys/class/net/{self.name}/phys_switch_id 2>/dev/null; echo ---; "
+ f"cat /sys/class/net/{self.name}/uevent").stdout
+ switch_id, _, uevent = out.partition("---")
+ self._switched = bool(switch_id.strip()) or "DEVTYPE=dsa" in uevent.split()
+
+ return self._switched
+
+ @property
+ def driver(self):
+ """The driver bound to the port, None when it has no device"""
+ if self._driver is None:
+ self._driver = self.ssh.runsh(
+ f"sed -n 's/^DRIVER=//p' /sys/class/net/{self.name}/device/uevent"
+ ).stdout.strip()
+
+ return self._driver or None
+
+ @property
+ def offload(self):
+ """The stages the port currently runs in hardware"""
+ return self.caps.get("offload", [])
+
+ @property
+ def traffic_classes(self):
+ """How many traffic classes the port has"""
+ return self.caps.get("max-traffic-classes", 8)
+
+ @property
+ def trust_orders(self):
+ """The classification trust orders the driver accepts, [] for none"""
+ return self.caps.get("supported-trust-order", [])
+
+ def offloads(self, stage):
+ """Whether the port runs @stage in hardware"""
+ return stage in self.offload
+
+ def require_offload(self, test, stage):
+ """Skip unless a fabric port runs @stage in hardware
+
+ A port the CPU serves needs no offload: the kernel does the work
+ and the measurement means what it says.
+ """
+ if self.switched and not self.offloads(stage):
+ test.skip(f"{self.name} forwards in hardware and its driver "
+ f"does not offload {stage}")
+
+ def require_classes(self, test, count):
+ """Skip unless the port has at least @count traffic classes"""
+ if self.traffic_classes < count:
+ test.skip(f"{self.name} has {self.traffic_classes} traffic "
+ f"classes, the test needs {count}")
diff --git a/test/infamy/qos.py b/test/infamy/qos.py
new file mode 100644
index 0000000000..e320ec63dd
--- /dev/null
+++ b/test/infamy/qos.py
@@ -0,0 +1,378 @@
+"""
+QoS helpers: capabilities, the scheduler qdisc, per-class counters,
+and traffic marked the way the tests need it.
+
+Traffic class numbering follows IEEE 802.1Q: class 0 is the lowest. The
+tc ets qdisc numbers its bands the other way, so everything here talks
+in traffic classes and hides that.
+"""
+import json
+import re
+import subprocess
+import time
+
+from infamy.util import until
+
+# IEEE 802.1Q-2022 Table 8-5 and Table 34-1, priority to traffic class,
+# indexed by the number of classes
+TABLE_8_5 = {
+ 2: [0, 0, 0, 0, 1, 1, 1, 1],
+ 3: [0, 0, 0, 0, 1, 1, 2, 2],
+ 4: [0, 0, 1, 1, 2, 2, 3, 3],
+ 5: [0, 0, 1, 1, 2, 2, 3, 4],
+ 6: [1, 0, 2, 2, 3, 3, 4, 5],
+ 7: [1, 0, 2, 3, 4, 4, 5, 6],
+ 8: [1, 0, 2, 3, 4, 5, 6, 7],
+}
+TABLE_34_1 = {
+ 2: [0, 0, 1, 1, 0, 0, 0, 0],
+ 3: [0, 0, 1, 2, 0, 0, 0, 0],
+ 4: [0, 0, 2, 3, 1, 1, 1, 1],
+ 5: [0, 0, 3, 4, 1, 1, 2, 2],
+ 6: [0, 0, 4, 5, 1, 1, 2, 3],
+ 7: [0, 0, 5, 6, 1, 2, 3, 4],
+ 8: [1, 0, 6, 7, 2, 3, 4, 5],
+}
+
+STRICT = "ieee802-dot1q-types:strict-priority"
+ETS = "ieee802-dot1q-types:enhanced-transmission-selection"
+
+ETS_QUANTUM_UNIT = 1514 # one frame per percent of bandwidth, as configured
+
+
+def xpath(port, path=""):
+ return f"/ietf-interfaces:interfaces/interface[name='{port}']/infix-interfaces:qos{path}"
+
+
+def dscp_map(**prio):
+ """A custom DSCP map: dscp_map(**{"0": 7, "46": 5}) marks DSCP 0 as
+ priority 7 and DSCP 46 as priority 5. Unlisted codepoints fall to
+ the port's default priority.
+
+ Classification at the ingress port is what decides the queue all the
+ way to the listener, on one chip as across a cascade or a network of
+ switches, so the tests steer their flows here rather than with a
+ class table on the egress port.
+ """
+ return {"entry": [{"dscp": int(dscp), "priority": p} for dscp, p in prio.items()]}
+
+
+def traffic_classes(weighted, strict=()):
+ """The traffic-class list for a layout: weighted is {class: percent}
+
+ The shares are TCBandwidth percentages and must sum to 100, so 2:1
+ is {1: 67, 0: 33}.
+ """
+ classes = [{"id": tc, "algorithm": STRICT} for tc in strict]
+ for tc, share in weighted.items():
+ classes.append({"id": tc, "algorithm": ETS, "bandwidth": share})
+ return classes
+
+
+def qdiscs(ssh, port):
+ out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout
+ return json.loads(out or "[]")
+
+
+def root_qdisc(ssh, port):
+ for qdisc in qdiscs(ssh, port):
+ if qdisc.get("root"):
+ return qdisc
+ return None
+
+
+def scheduler(ssh, port):
+ """The ets qdisc: the root, or the child of a tbf root"""
+ root = root_qdisc(ssh, port)
+ if root and root["kind"] == "tbf":
+ for qdisc in qdiscs(ssh, port):
+ if qdisc.get("parent") == "1:1":
+ return qdisc
+ return None
+ return root
+
+
+def scheduler_matches(qdisc, num_tc, prio_map, strict=None, quanta=None):
+ """Check a scheduler qdisc against a priority to class map and layout
+
+ strict is the number of strict classes and quanta the shares of the
+ others from the top down; either left as None is not checked.
+ """
+ if not qdisc:
+ return False
+ opts = qdisc.get("options", {})
+ if qdisc["kind"] == "ets":
+ if opts.get("bands") != num_tc:
+ return False
+ if opts.get("priomap", [])[:8] != [num_tc - 1 - tc for tc in prio_map]:
+ return False
+ if strict is not None and opts.get("strict") != strict:
+ return False
+ if quanta is not None and opts.get("quanta", []) != list(quanta):
+ return False
+ return True
+ return False
+
+
+def class_stats(ssh, port, num_tc):
+ """Per traffic class counters of the scheduler: {tc: {packets, bytes, drops}}
+
+ Read from tc -s class show. iproute2 6.14 prints classes as JSON,
+ older releases ignore -j for classes and print text, so both are
+ parsed.
+ """
+ out = ssh.runsh(f"tc -s -j class show dev {port}").stdout.strip()
+ stats = {}
+
+ def tc_of(kind, handle):
+ minor = int(handle.split(":")[1], 16)
+ if kind == "ets":
+ return num_tc - minor # band 0, minor 1, is the top class
+ return None
+
+ if out.startswith("["):
+ for cls in json.loads(out):
+ tc = tc_of(cls.get("class"), cls.get("handle", "0:0"))
+ if tc is not None:
+ st = cls.get("stats", cls) # tc 6.14 nests the counters
+ stats[tc] = {"packets": st.get("packets", 0),
+ "bytes": st.get("bytes", 0),
+ "drops": st.get("drops", 0)}
+ return stats
+
+ current = None
+ for line in out.splitlines():
+ head = re.match(r"class (\S+) (\S+)", line)
+ if head:
+ current = tc_of(head.group(1), head.group(2))
+ continue
+ sent = re.match(r"\s*Sent (\d+) bytes (\d+) pkt \(dropped (\d+)", line)
+ if sent and current is not None:
+ stats[current] = {"bytes": int(sent.group(1)), "packets": int(sent.group(2)),
+ "drops": int(sent.group(3))}
+ return stats
+
+
+def stats_delta(before, after):
+ return {tc: {k: after[tc][k] - before.get(tc, {}).get(k, 0) for k in after[tc]}
+ for tc in after}
+
+
+def neighbour_mac(ns, ip):
+ """MAC of ip from the namespace's neighbour table, None if unresolved"""
+ out = ns.runsh(f"ip -j neigh show {ip}").stdout
+ for entry in json.loads(out or "[]"):
+ if entry.get("lladdr"):
+ return entry["lladdr"]
+ return None
+
+
+def mausezahn(ns, iface, src_ip, dst_ip, dst_mac, count=10, delay="5msec",
+ vid=None, pcp=None, dscp=None, dport=7777):
+ """Send count UDP datagrams from the namespace with an exact marking
+
+ vid and pcp add a VLAN tag with that PCP; dscp sets the IP DSCP.
+ mausezahn writes the frame itself, so the source address is given
+ rather than taken from a VLAN device it knows nothing about.
+ """
+ cmd = ["mausezahn", iface, "-c", str(count), "-d", delay,
+ "-A", src_ip, "-B", dst_ip, "-b", dst_mac]
+ if vid is not None:
+ cmd += ["-Q", f"{pcp or 0}:{vid}"]
+ params = f"dp={dport}"
+ if dscp is not None:
+ params += f",dscp={dscp}"
+ cmd += ["-t", "udp", params]
+ return ns.run(cmd, check=True, text=True, capture_output=True)
+
+
+def supported_pmd_types(target, port):
+ """PMD types the port can negotiate, empty when it has no PHY"""
+ data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']")
+ for iface in data["interfaces"]["interface"]:
+ eth = iface.get("ieee802-ethernet-interface:ethernet") or iface.get("ethernet") or {}
+ return eth.get("infix-ethernet-interface:supported-pmd-types", [])
+ return []
+
+
+def slow_port(port, until):
+ """Make the port the bottleneck, return the rate it drains in bit/s
+
+ A port with a PHY that can do 100BASE-TX is negotiated down to it,
+ so the queues fill against a real link. A port without a PHY, as on
+ a virtual rig, gets a 10 Mbit/s rate limit instead, which is what
+ puts its queues under load. Returns 0 when neither is possible.
+ """
+ pmd = "ieee802-ethernet-phy-type:pmd-type-100BASE-TX"
+ pmds = supported_pmd_types(port.target, port.name)
+
+ if pmd in pmds:
+ port.target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port.name,
+ "ethernet": {"auto-negotiation": {
+ "infix-ethernet-interface:advertised-pmd-types": [pmd]}}
+ }]
+ }
+ }})
+
+ def linked():
+ out = port.ssh.runsh(f"ip -j link show {port.name}").stdout
+ link = json.loads(out or "[]")
+ return (link and "LOWER_UP" in link[0].get("flags", [])
+ and scheduler(port.ssh, port.name))
+
+ until(linked, attempts=60)
+ print(port.ssh.runsh(f"ethtool {port.name} | grep -i speed").stdout.strip())
+ return 100_000_000
+
+ if not pmds:
+ port.target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port.name,
+ "infix-interfaces:qos": {"egress": {"rate-limit": {"rate": 10_000_000}}}
+ }]
+ }
+ }})
+ until(lambda: (root_qdisc(port.ssh, port.name) or {}).get("kind") == "tbf")
+ show_shaper(port)
+ return 10_000_000
+
+ return 0
+
+
+def dscp_name_to_num(name):
+ """dcb prints DSCP by name when it knows one: CS1, AF21, EF ..."""
+ if name.startswith("CS"):
+ return int(name[2:]) * 8
+ if name.startswith("AF"):
+ return int(name[2]) * 8 + int(name[3]) * 2
+ if name == "EF":
+ return 46
+ return int(name)
+
+
+def dscp_prio(ssh, port, dscp):
+ """The priority the port classifies a DSCP to, or None when unmapped
+
+ Read from the DCB table on a port whose driver has one, otherwise
+ from the flower rules of the software classifier.
+ """
+ out = ssh.runsh(f"dcb app show dev {port} 2>/dev/null").stdout
+ for line in out.splitlines():
+ name, _, rest = line.partition(" ")
+ if name.rstrip(":") != "dscp-prio":
+ continue
+ for token in rest.split():
+ key, _, prio = token.partition(":")
+ if dscp_name_to_num(key) == dscp:
+ return int(prio)
+ return None
+
+ out = ssh.runsh(f"tc -j filter show dev {port} ingress").stdout
+ for flt in json.loads(out or "[]"):
+ opts = flt.get("options") or {}
+ tos = str(opts.get("keys", {}).get("ip_tos", ""))
+ if not tos or int(tos.split("/")[0], 0) >> 2 != dscp:
+ continue
+ for act in opts.get("actions", []):
+ if act.get("kind") == "skbedit" and "priority" in act:
+ prio = str(act["priority"])
+ return 0 if prio == "none" else int(prio.rsplit(":", 1)[-1] or "0", 16)
+ return None
+
+
+def show_offload(port, stage="transmission-selection"):
+ """Log what the fabric took after a change: the offload list and the
+ driver's recent complaints. A fabric port must still run @stage in
+ hardware, or the measurement that follows means nothing
+ """
+ if port.switched:
+ until(lambda: port.offloads(stage))
+ print(f"{port.name} offload: {port.offload}")
+
+ if not port.driver:
+ return
+
+ log = port.ssh.runsh(f"sudo dmesg | grep -i '{port.driver}' | tail -5").stdout.strip()
+ if log:
+ print(log)
+
+
+def show_shaper(port):
+ """Log what the rate limit became: the root qdisc with its offloaded
+ flag, and on a fabric port the port registers, where a shaper the
+ driver took shows up as the egress rate control words"""
+ print(json.dumps(root_qdisc(port.ssh, port.name)))
+ print(port.ssh.runsh(f"ethtool {port.name} | grep -i speed").stdout.strip())
+ if port.switched:
+ regs = port.ssh.runsh(f"sudo ethtool -d {port.name}").stdout.strip()
+ print("\n".join(regs.splitlines()[:16]))
+
+
+class Flow:
+ """One iperf3 UDP flow, talker to listener, told apart by its port"""
+ def __init__(self, name, port, tos, rate_bps, seconds=4, size=1000):
+ self.name, self.port, self.tos = name, port, tos
+ self.rate, self.seconds, self.size = rate_bps, seconds, size
+ self.result = None
+
+ def throughput(self):
+ """Payload bit rate that arrived, over the flow's duration"""
+ return 8.0 * self.result["packets"] * self.size / self.seconds
+
+ def share(self, total):
+ return 100.0 * self.result["packets"] / total if total else 0.0
+
+
+def run_flows(talker, listener, dst, flows):
+ """Run the flows at once, one server and one client process each
+
+ Fills in flow.result from the listener side: bytes and packets
+ received, lost packets and the loss in percent. Returns the total
+ datagrams received across the flows.
+ """
+ servers = {}
+ for f in flows:
+ servers[f.name] = listener.popen(["iperf3", "-s", "-1", "--json", "-p", str(f.port)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ text=True)
+ time.sleep(1)
+
+ clients = {}
+ for f in flows:
+ clients[f.name] = talker.popen(["iperf3", "-c", dst, "-p", str(f.port), "-u",
+ "-b", str(f.rate), "-t", str(f.seconds),
+ "-l", str(f.size), "--tos", str(f.tos), "--json"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ text=True)
+
+ offered = {}
+ for f in flows:
+ out, err = clients[f.name].communicate(timeout=f.seconds + 30)
+ if clients[f.name].returncode:
+ print(f"{f.name}: iperf3 client failed: {err.strip() or out.strip()[:200]}")
+ offered[f.name] = json.loads(out)["end"]["sum"]["packets"]
+
+ total = 0
+ for f in flows:
+ out, err = servers[f.name].communicate(timeout=30)
+ end = json.loads(out)["end"]
+ got = end.get("sum_received") or end["sum"]
+ # iperf3's receiver keeps its packet count as the highest sequence
+ # number seen, so it hides every loss but a tail drop. The byte
+ # count is what arrived, and datagrams are one size, so count
+ # those; offered minus received is the honest loss.
+ sent = offered[f.name]
+ received = got["bytes"] // f.size
+ lost = max(sent - received, 0)
+ f.result = {"bytes": got["bytes"], "packets": received,
+ "offered": sent, "lost": lost,
+ "lost_percent": 100.0 * lost / sent if sent else 0.0}
+ total += f.result["packets"]
+ print(f"{f.name}: {f.result['packets']}/{sent} datagrams through, "
+ f"{lost} lost ({f.result['lost_percent']:.1f}%)")
+
+ return total
diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py
index de41da0044..3a4811f43f 100644
--- a/test/infamy/restconf.py
+++ b/test/infamy/restconf.py
@@ -82,6 +82,9 @@ def requests_workaround(method, url, json, headers, auth, verify=False, retry=0,
response = requests_workaround(method, url, json, headers, auth,
verify, retry, timeout)
else:
+ # The RESTCONF error body names the offending node and why
+ if response.text:
+ print(f"{method} {url}: HTTP {response.status_code}: {response.text.strip()}")
raise e
return response
diff --git a/test/infamy/tap.py b/test/infamy/tap.py
index 2ff55074db..91340876e1 100644
--- a/test/infamy/tap.py
+++ b/test/infamy/tap.py
@@ -100,7 +100,7 @@ def step(self, msg):
if type(e) == TestPass:
self._ok(msg)
elif type(e) == TestSkip:
- self._ok(directive="skip")
+ self._ok(directive=f"skip {e}".strip())
elif type(e) == TestFail:
self._not_ok(msg)
else:
@@ -128,8 +128,9 @@ def _not_ok(self, msg):
def succeed(self):
raise TestPass()
- def skip(self):
- raise TestSkip()
+ def skip(self, reason=""):
+ """End the test as skipped, stating why in the report"""
+ raise TestSkip(reason)
def fail(self, message=None):
if message: