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 f90721134..70098394c 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 b7c4fe051..c23012847 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 195d5534c..b66bf32cb 100644
--- a/board/aarch64/microchip-ev23x71a/README.md
+++ b/board/aarch64/microchip-ev23x71a/README.md
@@ -26,7 +26,9 @@ 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. Ingress
+ classification (`dcb`) and traffic classes (`mqprio`) are configurable
+ from the QoS model but not yet verified on this board
- HSR/PRP offload, see above
- MAC addresses. With no environment in flash the board falls back to
`lan969x_otp_get_mac()`, which derives 30 addresses for this board
diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig
index 3c0639ad7..1ed746af3 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 875dcc0b9..000000000
--- 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 080eca13e..c5938d6d1 100644
--- a/board/riscv64/linux_defconfig
+++ b/board/riscv64/linux_defconfig
@@ -193,13 +193,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 4b9f5d050..3a2e89380 100644
--- a/board/x86_64/linux_defconfig
+++ b/board/x86_64/linux_defconfig
@@ -219,9 +219,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 d6d9fa15a..c4e3f7731 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -16,6 +16,24 @@ All notable changes to the project are documented in this file.
- Document the release and maintenance policy: which versions receive
updates, what may go into a patch release, and the levels of long-term
maintenance available, see [Releases & Support][relsup]
+- 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](qos.md)
+- Add a per-port egress rate limit under
+ `/interfaces/interface/qos/egress/rate-limit`, one token bucket above
+ the traffic classes, see [QoS](qos.md)
+- Transmission selection is rendered with 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
### Added
diff --git a/doc/extra.css b/doc/extra.css
index 3e9c836df..7df85d675 100644
--- a/doc/extra.css
+++ b/doc/extra.css
@@ -102,14 +102,21 @@ body {
--md-primary-bg-color: #5c5f5c;
}
-/* Center Markdown Tables (requires md_in_html extension) */
-.center-table {
+/* 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 */
diff --git a/doc/img/qos-pipeline.svg b/doc/img/qos-pipeline.svg
new file mode 100644
index 000000000..88c256ac0
--- /dev/null
+++ b/doc/img/qos-pipeline.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/doc/qos.md b/doc/qos.md
index d937104f6..48ae70ee7 100644
--- a/doc/qos.md
+++ b/doc/qos.md
@@ -2,93 +2,509 @@ 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 |
+|---------:|-----------------------|
+| 0 | Best Effort (default) |
+| 1 | Background |
+| 2 | Excellent Effort |
+| 3 | Critical Applications |
+| 4 | Video |
+| 5 | Voice |
+| 6 | Internetwork Control |
+| 7 | Network Control |
+/// 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 |
+|--------------------------|--------------------------------|----------------|
+| Default Forwarding, DF | 0, also written CS0 | RFC 4594 1.5.1 |
+| Class Selector, CS1..7 | 8, 16, 24, 32, 40, 48, 56 | RFC 4594 1.5.4 |
+| Assured Forwarding, AFxy | 10..14, 18..22, 26..30, 34..38 | RFC 4594 1.5.2 |
+| Expedited Forwarding, EF | 46 | RFC 4594 1.5.3 |
+/// 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 and 5, and the remarking half of stage 6, are configurable
+today and described below. The others show where later features
+attach; the figure is the intended pipeline, not a promise of hardware
+support for every stage.
+
+
+## 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 |
+
+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 | `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_ |
+
+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/e1/qos/ingress/> set trust dscp-pcp
+admin@example:/config/interface/e1/qos/ingress/> set default-priority 0
+admin@example:/config/interface/e1/qos/ingress/> set dscp-map preset ietf
+admin@example:/config/interface/e1/qos/ingress/> set pcp-map preset ieee
+admin@example:/config/interface/e1/qos/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/e2/qos/ingress/> set trust dscp
+admin@example:/config/interface/e2/qos/ingress/> set default-priority 0
+admin@example:/config/interface/e2/qos/ingress/> set dscp-map entry 46 priority 5
+admin@example:/config/interface/e2/qos/ingress/> set dscp-map entry 26 priority 3
+admin@example:/config/interface/e2/qos/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 |
+
+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/e1/qos/egress/> set traffic-class 3 algorithm strict-priority
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 2 algorithm strict-priority
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 1 algorithm enhanced-transmission-selection
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 1 bandwidth 67
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 0 algorithm enhanced-transmission-selection
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 0 bandwidth 33
+admin@example:/config/interface/e1/qos/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.
+
+
+
+The limit is rendered as a `tc tbf` root qdisc with the scheduler below
+it, so on a port without offload it applies to the frames the CPU
+sends, and on a port whose driver takes the bucket to frames the fabric
+forwards as well; `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 and DEI on transmit |
+| `dscp` | `from-priority` writes DSCP on transmit |
+
+PCP is set to the priority, 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[^16]. With the default `pcp-map` the two are the same:
+
+
admin@example:/config/> edit interface e1 qos egress remark
+admin@example:/config/interface/e1/qos/egress/remark/> set pcp from-priority
+admin@example:/config/interface/e1/qos/egress/remark/> set dscp from-priority
+admin@example:/config/interface/e1/qos/egress/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 | 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 | 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; the credit-based
+shaper for the SR classes is a later addition:
+
+
+
+
+## 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 | Meaning |
+|-------------------------|--------------------------------------------|
+| `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 |
+
+`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 mechanism | Driver hook | Without it |
+|------------------------|-----------------|---------------------|----------------------|
+| 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.
+
+Driver support in the Linux kernel, as of 6.18:
+
+| Driver | Classification | Remarking | Traffic classes |
+|-------------------------------------|----------------|---------------|-----------------|
+| Microchip `sparx5`, `lan966x` | hardware | hardware | hardware |
+| DSA `mv88e6xxx`, Marvell LinkStreet | hardware[^15] | hardware[^15] | hardware[^15] |
+| Data-center NICs[^12] | software[^14] | DSCP, software| hardware |
+| DSA `felix`, `ksz` | software[^14] | DSCP, software| hardware |
+| Other NICs and SoC MACs[^13] | software | DSCP, software| software |
+/// table-caption
+QoS support per driver family.
+///
+
+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. Per-board notes live in the board's `README.md` under `board/`.
### 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. Two 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. Three
+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]: Intel `ixgbe`, `i40e`, `ice`, Mellanox `mlx5`, Broadcom `bnxt`,
+ Chelsio `cxgb4`, Marvell `qede`, HiSilicon `hns3`, Netronome `nfp`
+[^13]: E.g. Raspberry Pi `bcmgenet`, MediaTek `mtk_eth_soc`, Intel `igb`
+ and `e1000`, and `virtio_net` in QEMU
+[^14]: These drivers take a DSCP map but not the PCP map or trust order;
+ the table is programmed as a whole, so it falls back to software
+[^15]: 88E6390 and 88E6393X generations, through patches carried by the
+ system until they land upstream. Older generations classify by
+ their hardware defaults and are offloaded like `felix` and `ksz`.
+[^16]: 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/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch b/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch
new file mode 100644
index 000000000..ee419346a
--- /dev/null
+++ b/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch
@@ -0,0 +1,45 @@
+From b0b26eb2286e08e25cb49e75f967105ba162f7d3 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:04:21 +0200
+Subject: [PATCH 69/77] net: dsa: Skip DCB default priority init on unsupported
+ switches
+Organization: Wires
+
+A driver serving several chip generations has one dsa_switch_ops for
+all of them, so port_get_default_prio is set even for chips that have
+no per-port default priority. Returning -EOPNOTSUPP from it today
+fails DCB init, and with it user port registration.
+
+Treat -EOPNOTSUPP as "no entry", as the DSCP init loop already does.
+
+Signed-off-by: Joachim Wiberg
+---
+ net/dsa/user.c | 11 ++++++-----
+ 1 file changed, 6 insertions(+), 5 deletions(-)
+
+diff --git a/net/dsa/user.c b/net/dsa/user.c
+index c0019d0c1172d..b881c177628ce 100644
+--- a/net/dsa/user.c
++++ b/net/dsa/user.c
+@@ -2511,12 +2511,13 @@ static int dsa_user_dcbnl_init(struct net_device *dev)
+ .priority = prio,
+ };
+
+- if (prio < 0)
++ if (prio >= 0) {
++ err = dcb_ieee_setapp(dev, &app);
++ if (err)
++ return err;
++ } else if (prio != -EOPNOTSUPP) {
+ return prio;
+-
+- err = dcb_ieee_setapp(dev, &app);
+- if (err)
+- return err;
++ }
+ }
+
+ if (ds->ops->port_get_dscp_prio) {
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch b/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch
new file mode 100644
index 000000000..257168fe5
--- /dev/null
+++ b/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch
@@ -0,0 +1,172 @@
+From 797dd2550e75613fb6eade3f1244be137a8113b4 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:04:22 +0200
+Subject: [PATCH 70/77] net: dsa: Support the PCP APP selector
+Organization: Wires
+
+The DCB APP table can map PCP and DEI to priority through the
+DCB_APP_SEL_PCP selector, but DSA only dispatches the EtherType and
+DSCP selectors, so 'dcb app add dev lan1 pcp-prio 5nd:5' fails with
+EOPNOTSUPP on any DSA user port.
+
+Add port_add_pcp_prio, port_del_pcp_prio and port_get_pcp_prio switch
+ops and route the selector to them, mirroring the DSCP handling: the
+highest priority among the entries for a PCP/DEI pair is programmed,
+and the table is pre-populated from hardware at init. The protocol
+field carries DEI in bit 3 and PCP in bits 2:0, as the userspace
+'dcb' tool encodes it.
+
+Signed-off-by: Joachim Wiberg
+---
+ include/net/dsa.h | 6 +++
+ net/dsa/user.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 102 insertions(+)
+
+diff --git a/include/net/dsa.h b/include/net/dsa.h
+index c31d4e910f075..03def1d584c68 100644
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -962,6 +962,12 @@ struct dsa_switch_ops {
+ u8 prio);
+ int (*port_del_etype_prio)(struct dsa_switch *ds, int port, u16 etype,
+ u8 prio);
++ int (*port_get_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp,
++ u8 dei);
++ int (*port_add_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp,
++ u8 dei, u8 prio);
++ int (*port_del_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp,
++ u8 dei, u8 prio);
+
+ /*
+ * Suspend and resume
+diff --git a/net/dsa/user.c b/net/dsa/user.c
+index b881c177628ce..1060dcff42096 100644
+--- a/net/dsa/user.c
++++ b/net/dsa/user.c
+@@ -2340,6 +2340,72 @@ dsa_user_dcbnl_add_dscp_prio(struct net_device *dev, struct dcb_app *app)
+ return 0;
+ }
+
++/* The protocol of a PCP APP entry is the DEI in bit 3 and the PCP in
++ * bits 2:0, matching the order of the DCB_APP_SEL_PCP table.
++ */
++#define DSA_DCB_PCP_MAX 16
++#define DSA_DCB_PCP(proto) ((proto) & 0x7)
++#define DSA_DCB_DEI(proto) (((proto) >> 3) & 0x1)
++
++static int __maybe_unused
++dsa_user_dcbnl_add_pcp_prio(struct net_device *dev, struct dcb_app *app)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ unsigned long mask, new_prio;
++ int err, port = dp->index;
++
++ if (!ds->ops->port_add_pcp_prio)
++ return -EOPNOTSUPP;
++
++ if (app->protocol >= DSA_DCB_PCP_MAX) {
++ netdev_err(dev, "PCP APP entry with protocol value %u is invalid\n",
++ app->protocol);
++ return -EINVAL;
++ }
++
++ err = dcb_ieee_setapp(dev, app);
++ if (err)
++ return err;
++
++ mask = dcb_ieee_getapp_mask(dev, app);
++ new_prio = __fls(mask);
++
++ err = ds->ops->port_add_pcp_prio(ds, port, DSA_DCB_PCP(app->protocol),
++ DSA_DCB_DEI(app->protocol), new_prio);
++ if (err) {
++ dcb_ieee_delapp(dev, app);
++ return err;
++ }
++
++ return 0;
++}
++
++static int __maybe_unused
++dsa_user_dcbnl_del_pcp_prio(struct net_device *dev, struct dcb_app *app)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ int err, port = dp->index;
++
++ if (!ds->ops->port_del_pcp_prio)
++ return -EOPNOTSUPP;
++
++ err = dcb_ieee_delapp(dev, app);
++ if (err)
++ return err;
++
++ err = ds->ops->port_del_pcp_prio(ds, port, DSA_DCB_PCP(app->protocol),
++ DSA_DCB_DEI(app->protocol),
++ app->priority);
++ if (err) {
++ dcb_ieee_setapp(dev, app);
++ return err;
++ }
++
++ return 0;
++}
++
+ static int __maybe_unused
+ dsa_user_dcbnl_set_etype_prio(struct net_device *dev, struct dcb_app *app)
+ {
+@@ -2382,6 +2448,8 @@ static int __maybe_unused dsa_user_dcbnl_ieee_setapp(struct net_device *dev,
+ break;
+ case IEEE_8021QAZ_APP_SEL_DSCP:
+ return dsa_user_dcbnl_add_dscp_prio(dev, app);
++ case DCB_APP_SEL_PCP:
++ return dsa_user_dcbnl_add_pcp_prio(dev, app);
+ default:
+ return -EOPNOTSUPP;
+ }
+@@ -2488,6 +2556,8 @@ static int __maybe_unused dsa_user_dcbnl_ieee_delapp(struct net_device *dev,
+ break;
+ case IEEE_8021QAZ_APP_SEL_DSCP:
+ return dsa_user_dcbnl_del_dscp_prio(dev, app);
++ case DCB_APP_SEL_PCP:
++ return dsa_user_dcbnl_del_pcp_prio(dev, app);
+ default:
+ return -EOPNOTSUPP;
+ }
+@@ -2544,6 +2614,32 @@ static int dsa_user_dcbnl_init(struct net_device *dev)
+ }
+ }
+
++ if (ds->ops->port_get_pcp_prio) {
++ int protocol;
++
++ for (protocol = 0; protocol < DSA_DCB_PCP_MAX; protocol++) {
++ struct dcb_app app = {
++ .selector = DCB_APP_SEL_PCP,
++ .protocol = protocol,
++ };
++ int prio;
++
++ prio = ds->ops->port_get_pcp_prio(ds, port,
++ DSA_DCB_PCP(protocol),
++ DSA_DCB_DEI(protocol));
++ if (prio == -EOPNOTSUPP)
++ continue;
++ if (prio < 0)
++ return prio;
++
++ app.priority = prio;
++
++ err = dcb_ieee_setapp(dev, &app);
++ if (err)
++ return err;
++ }
++ }
++
+ return 0;
+ }
+
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch b/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch
new file mode 100644
index 000000000..0fee7bf3e
--- /dev/null
+++ b/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch
@@ -0,0 +1,259 @@
+From c2319721ee7343981dcd0c57fd16a651db865381 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:10:32 +0200
+Subject: [PATCH 71/77] net: dsa: Support DCB priority rewrite
+Organization: Wires
+
+The DCB rewrite table maps a priority back to the PCP and DEI, or the
+DSCP, that frames are remarked with on egress. DSA has no
+dcbnl_setrewr/delrewr, so 'dcb rewr add' on a user port falls through
+to the generic table and succeeds without touching hardware.
+
+Add port_set_pcp_rewr/port_del_pcp_rewr and the DSCP counterparts,
+plus getters to pre-populate the table from hardware at init. A
+switch holds one code point per priority and selector, so a new entry
+replaces any existing one for that priority, as sparx5 does.
+
+Signed-off-by: Joachim Wiberg
+---
+ include/net/dsa.h | 10 +++
+ net/dsa/user.c | 188 +++++++++++++++++++++++++++++++++++++++++++++-
+ 2 files changed, 197 insertions(+), 1 deletion(-)
+
+diff --git a/include/net/dsa.h b/include/net/dsa.h
+index 03def1d584c68..eec90f59b4983 100644
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -968,6 +968,16 @@ struct dsa_switch_ops {
+ u8 dei, u8 prio);
+ int (*port_del_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp,
+ u8 dei, u8 prio);
++ int (*port_get_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio,
++ u8 *pcp, u8 *dei);
++ int (*port_set_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio,
++ u8 pcp, u8 dei);
++ int (*port_del_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio);
++ int (*port_get_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio,
++ u8 *dscp);
++ int (*port_set_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio,
++ u8 dscp);
++ int (*port_del_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio);
+
+ /*
+ * Suspend and resume
+diff --git a/net/dsa/user.c b/net/dsa/user.c
+index 1060dcff42096..c22fa40ff23a0 100644
+--- a/net/dsa/user.c
++++ b/net/dsa/user.c
+@@ -2563,6 +2563,190 @@ static int __maybe_unused dsa_user_dcbnl_ieee_delapp(struct net_device *dev,
+ }
+ }
+
++/* Egress remarking: the DCB rewrite table maps a priority back to a PCP
++ * and DEI, or to a DSCP. Hardware holds one code point per priority and
++ * selector, so a new entry replaces any existing one for that priority.
++ */
++static int __maybe_unused
++dsa_user_dcbnl_rewr_validate(struct net_device *dev, struct dcb_app *app)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ u16 max;
++
++ switch (app->selector) {
++ case DCB_APP_SEL_PCP:
++ if (!ds->ops->port_set_pcp_rewr || !ds->ops->port_del_pcp_rewr)
++ return -EOPNOTSUPP;
++ max = DSA_DCB_PCP_MAX;
++ break;
++ case IEEE_8021QAZ_APP_SEL_DSCP:
++ if (!ds->ops->port_set_dscp_rewr || !ds->ops->port_del_dscp_rewr)
++ return -EOPNOTSUPP;
++ max = 64;
++ break;
++ default:
++ return -EOPNOTSUPP;
++ }
++
++ if (app->protocol >= max || app->priority >= IEEE_8021QAZ_MAX_TCS) {
++ netdev_err(dev, "Rewrite entry %u:%u is invalid\n",
++ app->priority, app->protocol);
++ return -EINVAL;
++ }
++
++ return 0;
++}
++
++static int __maybe_unused
++dsa_user_dcbnl_rewr_program(struct net_device *dev, struct dcb_app *app)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ int port = dp->index;
++
++ if (app->selector == DCB_APP_SEL_PCP)
++ return ds->ops->port_set_pcp_rewr(ds, port, app->priority,
++ DSA_DCB_PCP(app->protocol),
++ DSA_DCB_DEI(app->protocol));
++
++ return ds->ops->port_set_dscp_rewr(ds, port, app->priority,
++ app->protocol);
++}
++
++static int __maybe_unused
++dsa_user_dcbnl_rewr_clear(struct net_device *dev, struct dcb_app *app)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ int port = dp->index;
++
++ if (app->selector == DCB_APP_SEL_PCP)
++ return ds->ops->port_del_pcp_rewr(ds, port, app->priority);
++
++ return ds->ops->port_del_dscp_rewr(ds, port, app->priority);
++}
++
++/* Protocols currently rewritten for the priority and selector of @app */
++static u64 __maybe_unused
++dsa_user_dcbnl_rewr_mask(struct net_device *dev, struct dcb_app *app)
++{
++ struct dcb_rewr_prio_pcp_map pcp_map;
++ struct dcb_ieee_app_prio_map dscp_map;
++
++ if (app->selector == DCB_APP_SEL_PCP) {
++ dcb_getrewr_prio_pcp_mask_map(dev, &pcp_map);
++ return pcp_map.map[app->priority];
++ }
++
++ dcb_getrewr_prio_dscp_mask_map(dev, &dscp_map);
++ return dscp_map.map[app->priority];
++}
++
++static int __maybe_unused dsa_user_dcbnl_setrewr(struct net_device *dev,
++ struct dcb_app *app)
++{
++ struct dcb_app old = *app;
++ bool exists;
++ u64 mask;
++ int err;
++
++ err = dsa_user_dcbnl_rewr_validate(dev, app);
++ if (err)
++ return err;
++
++ mask = dsa_user_dcbnl_rewr_mask(dev, app);
++ exists = mask & BIT_ULL(app->protocol);
++
++ if (!exists) {
++ err = dcb_setrewr(dev, app);
++ if (err)
++ return err;
++ }
++
++ err = dsa_user_dcbnl_rewr_program(dev, app);
++ if (err) {
++ if (!exists)
++ dcb_delrewr(dev, app);
++ return err;
++ }
++
++ mask &= ~BIT_ULL(app->protocol);
++ for (old.protocol = 0; mask; old.protocol++, mask >>= 1)
++ if (mask & 1)
++ dcb_delrewr(dev, &old);
++
++ return 0;
++}
++
++static int __maybe_unused dsa_user_dcbnl_delrewr(struct net_device *dev,
++ struct dcb_app *app)
++{
++ int err;
++
++ err = dsa_user_dcbnl_rewr_validate(dev, app);
++ if (err)
++ return err;
++
++ err = dcb_delrewr(dev, app);
++ if (err)
++ return err;
++
++ err = dsa_user_dcbnl_rewr_clear(dev, app);
++ if (err) {
++ dcb_setrewr(dev, app);
++ return err;
++ }
++
++ return 0;
++}
++
++static int __maybe_unused dsa_user_dcbnl_rewr_init(struct net_device *dev)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_switch *ds = dp->ds;
++ int port = dp->index;
++ int err, prio;
++
++ for (prio = 0; prio < IEEE_8021QAZ_MAX_TCS; prio++) {
++ struct dcb_app app = { .priority = prio };
++ u8 pcp, dei, dscp;
++
++ if (ds->ops->port_get_pcp_rewr) {
++ err = ds->ops->port_get_pcp_rewr(ds, port, prio,
++ &pcp, &dei);
++ if (err && err != -ENOENT && err != -EOPNOTSUPP)
++ return err;
++
++ if (!err) {
++ app.selector = DCB_APP_SEL_PCP;
++ app.protocol = dei << 3 | pcp;
++
++ err = dcb_setrewr(dev, &app);
++ if (err)
++ return err;
++ }
++ }
++
++ if (ds->ops->port_get_dscp_rewr) {
++ err = ds->ops->port_get_dscp_rewr(ds, port, prio, &dscp);
++ if (err && err != -ENOENT && err != -EOPNOTSUPP)
++ return err;
++
++ if (!err) {
++ app.selector = IEEE_8021QAZ_APP_SEL_DSCP;
++ app.protocol = dscp;
++
++ err = dcb_setrewr(dev, &app);
++ if (err)
++ return err;
++ }
++ }
++ }
++
++ return 0;
++}
++
+ /* Pre-populate the DCB application priority table with the priorities
+ * configured during switch setup, which we read from hardware here.
+ */
+@@ -2640,7 +2824,7 @@ static int dsa_user_dcbnl_init(struct net_device *dev)
+ }
+ }
+
+- return 0;
++ return dsa_user_dcbnl_rewr_init(dev);
+ }
+
+ static const struct ethtool_ops dsa_user_ethtool_ops = {
+@@ -2683,6 +2867,8 @@ static const struct dcbnl_rtnl_ops __maybe_unused dsa_user_dcbnl_ops = {
+ .ieee_delapp = dsa_user_dcbnl_ieee_delapp,
+ .dcbnl_setapptrust = dsa_user_dcbnl_set_apptrust,
+ .dcbnl_getapptrust = dsa_user_dcbnl_get_apptrust,
++ .dcbnl_setrewr = dsa_user_dcbnl_setrewr,
++ .dcbnl_delrewr = dsa_user_dcbnl_delrewr,
+ };
+
+ static void dsa_user_get_stats64(struct net_device *dev,
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch b/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch
new file mode 100644
index 000000000..b840707bb
--- /dev/null
+++ b/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch
@@ -0,0 +1,736 @@
+From 7221886d68f43d884fca5b6adf9a9b62b4c8142b Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:10:33 +0200
+Subject: [PATCH 72/77] 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 1d1edf84b224e..bf5d544507c4f 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4847,6 +4847,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,
+@@ -4907,6 +4913,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,
+@@ -4967,6 +4979,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,
+@@ -5131,6 +5149,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,
+@@ -5519,6 +5543,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,
+@@ -5583,6 +5613,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,
+@@ -5646,6 +5682,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,
+@@ -7155,6 +7197,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)
+ {
+@@ -7341,6 +7548,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 5d5e7ab63fca3..bb5739cf8cf9d 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 843972077576c..f7991eeb9d17f 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 5f733f84b3481..a9f2ed6ca79cc 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,
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch b/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch
new file mode 100644
index 000000000..b356f568d
--- /dev/null
+++ b/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch
@@ -0,0 +1,433 @@
+From 25811498ff5773e47d29b27b2786082653941dde Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 17:12:40 +0200
+Subject: [PATCH 73/77] 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 bf5d544507c4f..1dac45b89754c 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4853,6 +4853,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,
+@@ -4919,6 +4923,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,
+@@ -4985,6 +4993,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,
+@@ -5155,6 +5167,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,
+@@ -5549,6 +5565,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,
+@@ -5619,6 +5639,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,
+@@ -5688,6 +5712,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,
+@@ -7336,6 +7364,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)
+ {
+@@ -7558,6 +7692,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 bb5739cf8cf9d..c0af84346b897 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 f7991eeb9d17f..92618049e889f 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 a9f2ed6ca79cc..ef5028707ec5a 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,
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch b/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch
new file mode 100644
index 000000000..d6d2b59ed
--- /dev/null
+++ b/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch
@@ -0,0 +1,398 @@
+From 48aee1f644470643a38cf236ae913fe08601010e Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Wed, 9 Sep 2026 18:31:30 +0200
+Subject: [PATCH 74/77] 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 1dac45b89754c..673f8b7523a08 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);
+
+@@ -3398,7 +3401,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;
+
+@@ -3600,6 +3603,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)
+@@ -4853,6 +4860,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,
+@@ -4923,6 +4931,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,
+@@ -4993,6 +5002,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,
+@@ -5167,6 +5177,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,
+@@ -5565,6 +5576,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,
+@@ -5639,6 +5651,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,
+@@ -5712,6 +5725,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,
+@@ -7551,12 +7565,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;
+@@ -7577,10 +7675,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 c0af84346b897..f0736aeebbb8a 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 92618049e889f..255f6229a13af 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 ef5028707ec5a..273dacfe24396 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,
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch b/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch
new file mode 100644
index 000000000..58184271f
--- /dev/null
+++ b/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch
@@ -0,0 +1,59 @@
+From b8d9f22c758ac3d7a8ff56ffabe0d886f84161a1 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Fri, 11 Sep 2026 15:54:16 +0200
+Subject: [PATCH 75/77] 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 55c296e0b5b0e..35595aa21f524 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;
+ }
+
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch b/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch
new file mode 100644
index 000000000..287e10887
--- /dev/null
+++ b/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch
@@ -0,0 +1,275 @@
+From 21d713883139d436c6e58e2ae326bb303bd0dfe3 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 17 Sep 2026 17:09:02 +0200
+Subject: [PATCH 76/77] 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 673f8b7523a08..aab76fadf7530 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"
+@@ -4861,6 +4862,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,
+@@ -4932,6 +4934,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,
+@@ -5003,6 +5006,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,
+@@ -5178,6 +5182,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,
+@@ -5577,6 +5582,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,
+@@ -5652,6 +5658,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,
+@@ -5726,6 +5733,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,
+@@ -7685,7 +7693,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)
+@@ -7711,6 +7761,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 f0736aeebbb8a..b443317008875 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 255f6229a13af..47834060cabf7 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 273dacfe24396..49674351b5329 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,
+--
+2.43.0
+
diff --git a/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch b/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch
new file mode 100644
index 000000000..22d3c5c9c
--- /dev/null
+++ b/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch
@@ -0,0 +1,554 @@
+From 08883b16de388b3e9780a54dbe3b9c828f356375 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 17 Sep 2026 17:09:45 +0200
+Subject: [PATCH 77/77] 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 aab76fadf7530..842a6a85ca294 100644
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -4862,6 +4862,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,
+@@ -4934,6 +4936,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,
+@@ -5006,6 +5010,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,
+@@ -5182,6 +5188,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,
+@@ -5582,6 +5590,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,
+@@ -5658,6 +5668,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,
+@@ -5733,6 +5745,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,
+@@ -7695,6 +7709,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)
+ {
+@@ -7761,6 +8015,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 b443317008875..7bf7cdea0bb12 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 3f16a7ef3fa28..a23541e3cb734 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 496269c26c508..7d2dda5e18666 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 47834060cabf7..be5aed565fafd 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 49674351b5329..1434174a09370 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,
+--
+2.43.0
+
diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am
index 7e9a8b74f..62c568b71 100644
--- a/src/confd/src/Makefile.am
+++ b/src/confd/src/Makefile.am
@@ -46,6 +46,7 @@ confd_plugin_la_SOURCES = \
if-vlan.c \
if-gre.c \
if-vxlan.c \
+ if-qos.c \
if-wifi.c \
if-wireguard.c \
keystore.c \
diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c
new file mode 100644
index 000000000..a333d89ac
--- /dev/null
+++ b/src/confd/src/if-qos.c
@@ -0,0 +1,843 @@
+/* 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 rendered; 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 rendered today, and
+ * the layout tc ets can render (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 rendering 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
+ * rendered 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_egress(FILE *fp, const char *ifname, struct qos_egress *eg)
+{
+ const char *attach = "root";
+ int i, nstrict = 0;
+
+ fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname);
+
+ /*
+ * The rate limit is one bucket on the whole port, so it takes
+ * the root and the scheduler hangs below it.
+ */
+ if (eg->rate) {
+ fprintf(fp, "tc 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)
+ return;
+
+ for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--)
+ nstrict++;
+
+ fprintf(fp, "tc 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 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;
+
+ fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname);
+ fprintf(fp, "tc qdisc del dev %s clsact 2>/dev/null\n", ifname);
+ fprintf(fp, "dcb app flush dev %s default-prio pcp-prio dscp-prio 2>/dev/null\n", ifname);
+ fprintf(fp, "dcb rewr flush dev %s%s prio-dscp 2>/dev/null\n", ifname,
+ qos_pcp_encoded(ifname) ? "" : " prio-pcp");
+ fprintf(fp, "dcb apptrust set dev %s order 2>/dev/null\n", ifname);
+ fputs("exit 0\n", fp);
+ fclose(fp);
+
+ return 0;
+}
+
+/*
+ * Whether a diff subtree carries a change to configuration. A leaf
+ * going back to its default shows up flagged default with the old value
+ * in metadata, which lydx_get_diff() reads; a deleted list entry or
+ * presence container always counts.
+ */
+static bool qos_has_change(struct lyd_node *node)
+{
+ struct lyd_node *child;
+ struct lydx_diff nd;
+
+ if (!node || !node->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 render 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 be1bc222b..f880c20c0 100644
--- a/src/confd/src/interfaces.c
+++ b/src/confd/src/interfaces.c
@@ -737,6 +737,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 */
err = err ? : netdag_gen_link_mtu(ip, dif);
err = err ? : netdag_gen_link_addr(ip, cif, dif);
diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h
index a7bb77c07..430d781ea 100644
--- a/src/confd/src/interfaces.h
+++ b/src/confd/src/interfaces.h
@@ -167,6 +167,10 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path);
/* if-vxlan.c */
int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
+/* 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 62ba37b9d..aeba520b0 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -28,7 +28,7 @@ MODULES=(
"iana-hardware@2018-03-13.yang"
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
"infix-hardware@2026-07-02.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-01-07.yang"
"infix-routing@2026-03-11.yang"
@@ -48,7 +48,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-06-11.yang -e vlan-filtering"
+ "infix-interfaces@2026-09-07.yang -e vlan-filtering -e rate-limit"
"ietf-crypto-types -e cleartext-symmetric-keys"
"infix-crypto-types@2026-02-14.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 b2f709971..9f436ca59 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 000000000..de37a5f4d
--- /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.";
+ }
+
+ /* Rendered 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.";
+
+ /* Rendered 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.";
+ }
+
+ /* Rendered 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.";
+ }
+
+ /*
+ * Rendered 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;
+ }
+ }
+ }
+ }
+
+ /* Rendered 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";
+ }
+
+ /* Rendered 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";
+ }
+ }
+
+ /* Rendered 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.";
+ }
+ }
+
+ /* Rendered 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 000000000..591c3dc64
--- /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 d86a9789d..83ae593d4 100644
--- a/src/confd/yang/confd/infix-interfaces.yang
+++ b/src/confd/yang/confd/infix-interfaces.yang
@@ -36,11 +36,19 @@ module infix-interfaces {
include infix-if-wifi;
include infix-if-wireguard;
include infix-if-ptp;
+ include infix-if-qos;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Linux bridge and lag extensions for ietf-interfaces.";
+ 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/confd/yang/confd/infix-interfaces@2026-06-11.yang b/src/confd/yang/confd/infix-interfaces@2026-09-07.yang
similarity index 100%
rename from src/confd/yang/confd/infix-interfaces@2026-06-11.yang
rename to src/confd/yang/confd/infix-interfaces@2026-09-07.yang
diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py
index f4ae79a2b..19143beaa 100644
--- a/src/statd/python/yanger/ietf_interfaces/link.py
+++ b/src/statd/python/yanger/ietf_interfaces/link.py
@@ -140,15 +140,74 @@ def ptp_capabilities(ifname, systemjson):
return result or None
-def interface(iplink, ipaddr, systemjson=None):
+# Trust orders a driver's dcb apptrust accepts, keyed by driver name; the
+# kernel has no query for it. Same table as confd's qos validation.
+QOS_TRUST_ORDERS = {
+ "sparx5-switch": ["pcp", "dscp", "dscp-pcp"],
+ "lan966x-switch": ["pcp", "dscp", "dscp-pcp"],
+ "mv88e6085": ["pcp", "dscp", "pcp-dscp", "dscp-pcp"],
+}
+
+
+def qos_capabilities(iplink, qdiscs):
+ """Return infix-interfaces:qos/capabilities dict for a link, or None"""
+ ifname = iplink["ifname"]
+ result = {}
+
+ # 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.
+ txq = iplink.get("num_tx_queues", 1)
+ if 1 < txq < 8:
+ result["max-traffic-classes"] = txq
+
+ driver = None
+ if uevent := HOST.read(f"/sys/class/net/{ifname}/device/uevent"):
+ for line in uevent.splitlines():
+ if line.startswith("DRIVER="):
+ driver = line[7:].strip()
+ if trust := QOS_TRUST_ORDERS.get(driver):
+ result["supported-trust-order"] = trust
+
+ # Stages the driver runs. DCB tables exist only on drivers with the
+ # operations, so only those ports are asked. ets and tbf report
+ # offloaded when the driver took them.
+ offload = []
+ if trust:
+ app = HOST.run_json(["dcb", "-j", "app", "show", "dev", ifname], {})
+ if app.get("dscp_prio") or app.get("pcp_prio"):
+ offload.append("classification")
+ rewr = HOST.run_json(["dcb", "-j", "rewr", "show", "dev", ifname], {})
+ if rewr.get("prio_pcp") or rewr.get("prio_dscp"):
+ offload.append("remarking")
+
+ for qdisc in qdiscs.get(ifname, []):
+ kind, offloaded = qdisc.get("kind"), qdisc.get("offloaded", False)
+ if kind == "ets" and offloaded:
+ offload.append("transmission-selection")
+ if kind == "tbf" and qdisc.get("root") and offloaded:
+ offload.append("rate-limit")
+
+ if offload:
+ result["offload"] = offload
+
+ return result or None
+
+
+def interface(iplink, ipaddr, systemjson=None, qdiscs=None):
interface = interface_common(iplink, ipaddr)
if systemjson is None:
systemjson = {}
+ if qdiscs is None:
+ qdiscs = {}
if ptpcap := ptp_capabilities(iplink["ifname"], systemjson):
interface["infix-interfaces:ptp-capabilities"] = ptpcap
+ if qoscap := qos_capabilities(iplink, qdiscs):
+ interface["infix-interfaces:qos"] = {"capabilities": qoscap}
+
match interface["type"]:
case "infix-if-type:bridge":
if br := bridge.bridge(iplink):
@@ -201,6 +260,11 @@ def interfaces(ifname=None):
addrs = common.ipaddrs(ifname)
systemjson = HOST.read_json("/run/system.json", {})
+ qdiscs = {}
+ for qdisc in HOST.run_json(["tc", "-j", "qdisc", "show"], []):
+ if dev := qdisc.get("dev"):
+ qdiscs.setdefault(dev, []).append(qdisc)
+
interfaces = []
for ifname, iplink in links.items():
if iplink.get("group") == "internal":
@@ -212,6 +276,6 @@ def interfaces(ifname=None):
ipaddr = addrs.get(ifname, {})
- interfaces.append(interface(iplink, ipaddr, systemjson))
+ interfaces.append(interface(iplink, ipaddr, systemjson, qdiscs))
return interfaces
diff --git a/test/.env b/test/.env
index 16ef67f13..98176d9f9 100644
--- a/test/.env
+++ b/test/.env
@@ -2,7 +2,7 @@
# shellcheck disable=SC2034,SC2154
# Current container image
-INFIX_TEST=ghcr.io/kernelkit/infix-test:2.11
+INFIX_TEST=ghcr.io/kernelkit/infix-test:2.12
ixdir=$(readlink -f "$testdir/..")
logdir=$(readlink -f "$testdir/.log")
diff --git a/test/case/interfaces/Readme.adoc b/test/case/interfaces/Readme.adoc
index 4e1c8b372..c0ec25b67 100644
--- a/test/case/interfaces/Readme.adoc
+++ b/test/case/interfaces/Readme.adoc
@@ -11,7 +11,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
@@ -105,6 +105,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 d6584c710..d2a290d06 100644
--- a/test/case/interfaces/all.yaml
+++ b/test/case/interfaces/all.yaml
@@ -50,6 +50,9 @@
- name: Layer-2 Multicast Filtering Tests
suite: igmp.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 000000000..e048105b8
--- /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 000000000..ae32c8412
--- /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 000000000..e4842de69
--- /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 000000000..aee454126
--- /dev/null
+++ b/test/case/interfaces/qos_bridge_classify/test.py
@@ -0,0 +1,229 @@
+#!/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 capabilities(target, port):
+ data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']"
+ "/infix-interfaces:qos/capabilities")
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+ return {}
+
+
+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")
+
+ dcb = bool(capabilities(target, td0).get("supported-trust-order"))
+ uevent = tgtssh.runsh(f"cat /sys/class/net/{td0}/uevent").stdout
+ dsa = "DEVTYPE=dsa" in uevent.split()
+ print(f"{td0}: DCB {'supported' if dcb else 'not supported'}, DSA port: {dsa}")
+ if dsa and not dcb:
+ print("switch forwards in hardware without DCB support, skipping")
+ test.skip()
+
+ 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: "classification" in capabilities(target, td0).get("offload", []))
+ until(lambda: "remarking" in capabilities(target, td1).get("offload", []))
+ remark_hw = "remarking" in capabilities(target, td1).get("offload", [])
+
+ 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..148b9d538
--- /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 000000000..fadc3a815
--- /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 000000000..7689a6778
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..f20cb09f2
--- /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 000000000..2cb9c9787
--- /dev/null
+++ b/test/case/interfaces/qos_factory_default/test.py
@@ -0,0 +1,104 @@
+#!/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 capabilities(target, port):
+ data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']"
+ "/infix-interfaces:qos/capabilities")
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+ return {}
+
+
+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 = capabilities(target, port)
+ 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 000000000..4f53d15af
--- /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 000000000..ff3d246be
--- /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 000000000..ae32c8412
--- /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 000000000..eb358d325
--- /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 rendering:
+
+ - 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 rendered 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 000000000..c6c05b4b2
--- /dev/null
+++ b/test/case/interfaces/qos_ingress_classify/test.py
@@ -0,0 +1,194 @@
+#!/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 rendering:
+
+ - 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 rendered 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 capabilities(target, port):
+ data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']"
+ "/infix-interfaces:qos/capabilities")
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+ return {}
+
+
+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")
+ dcb = bool(capabilities(target, port).get("supported-trust-order"))
+ 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(capabilities(target, port).get("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 "classification" not in capabilities(target, port).get("offload", [])
+
+ 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 000000000..4f53d15af
--- /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 000000000..ff3d246be
--- /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 000000000..ae32c8412
--- /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 000000000..4d1324fcb
--- /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 000000000..3fe99c650
--- /dev/null
+++ b/test/case/interfaces/qos_mixed_selection/test.py
@@ -0,0 +1,126 @@
+#!/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")
+
+ num_tc = qos.num_classes(target, td1)
+ print(f"{td1}: {num_tc} traffic classes")
+ if num_tc < 4:
+ print("no class left for the control traffic above a strict and two weighted, skipping")
+ test.skip()
+
+ uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout
+ dsa = "DEVTYPE=dsa" in uevent.split()
+ if dsa and "transmission-selection" not in qos.offload(target, td1):
+ print("switch fabric forwards past a scheduler its driver does not offload, skipping")
+ test.skip()
+
+ 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(target, tgtssh, td1, dsa)
+
+ with test.step("Slow the egress port so its queues can fill"):
+ drain = qos.slow_port(target, tgtssh, td1, 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..d7a2c651f
--- /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 rendered 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 000000000..5c688ef52
--- /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 rendered 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 = qos.num_classes(target, td0)
+ 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..2d9b4ebec
--- /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 000000000..58fdf75f8
--- /dev/null
+++ b/test/case/interfaces/qos_rate_limit/test.py
@@ -0,0 +1,104 @@
+#!/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")
+
+ 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(tgtssh, td1)
+
+ uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout
+ if "DEVTYPE=dsa" in uevent.split() and "rate-limit" not in qos.offload(target, td1):
+ print("switch fabric forwards past a rate limit its driver does not offload, skipping")
+ test.skip()
+
+ 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..bd2cfff3f
--- /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 000000000..9aaebc3d9
--- /dev/null
+++ b/test/case/interfaces/qos_strict_priority/test.py
@@ -0,0 +1,142 @@
+#!/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")
+
+ num_tc = qos.num_classes(target, td1)
+ print(f"{td1}: {num_tc} traffic classes, EF at priority {HIGH}, CS1 at priority {LOW}")
+
+ uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout
+ dsa = "DEVTYPE=dsa" in uevent.split()
+ if dsa and "transmission-selection" not in qos.offload(target, td1):
+ print("switch fabric forwards past a scheduler its driver does not offload, skipping")
+ test.skip()
+
+ 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(target, tgtssh, td1, 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 rendering 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(target, tgtssh, td1, dsa)
+ 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /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 000000000..ae32c8412
--- /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 000000000..0befc650b
--- /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 000000000..8622297f0
--- /dev/null
+++ b/test/case/interfaces/qos_traffic_class/test.py
@@ -0,0 +1,172 @@
+#!/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 capabilities(target, port):
+ data = target.get_data(qos_xpath(port, "/capabilities"))
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+ return {}
+
+
+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")
+
+ num_tc = capabilities(target, port).get("max-traffic-classes", 8)
+ print(f"{port}: {num_tc} traffic classes")
+ assert num_tc and 2 <= num_tc <= 8, f"max-traffic-classes {num_tc}"
+ offloaded = "transmission-selection" in capabilities(target, port).get("offload", [])
+
+ 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: ("transmission-selection" in capabilities(target, port).get("offload", []))
+ == 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 ("transmission-selection" in capabilities(target, port).get("offload", [])) == 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 000000000..4f53d15af
--- /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 000000000..ff3d246be
--- /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 000000000..ae32c8412
--- /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 000000000..0c44e9f1c
--- /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 render 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 000000000..92a06ac19
--- /dev/null
+++ b/test/case/interfaces/qos_validation/test.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""
+QoS Configuration Validation
+
+Verify that traffic class layouts the device cannot render 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 num_classes(target, port):
+ data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']"
+ "/infix-interfaces:qos/capabilities")
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {}).get("max-traffic-classes", 8)
+ return None
+
+
+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 = num_classes(target, port)
+ 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 000000000..4f53d15af
--- /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 000000000..ff3d246be
--- /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 000000000..ae32c8412
--- /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 000000000..1b0868609
--- /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 000000000..d3e7e3aa9
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/test.py
@@ -0,0 +1,148 @@
+#!/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")
+
+ num_tc = qos.num_classes(target, td1)
+ print(f"{td1}: {num_tc} traffic classes")
+ if num_tc < 3:
+ print("no class left for the control traffic above two weighted ones, skipping")
+ test.skip()
+
+ uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout
+ dsa = "DEVTYPE=dsa" in uevent.split()
+ if dsa and "transmission-selection" not in qos.offload(target, td1):
+ print("switch fabric forwards past a scheduler its driver does not offload, skipping")
+ test.skip()
+
+ 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(target, tgtssh, td1, 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(target, tgtssh, td1, dsa)
+ 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 000000000..f288f2f95
--- /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 000000000..9517e6397
--- /dev/null
+++ b/test/case/interfaces/qos_weighted_sharing/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
index 1267d7c05..1de3f6459 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -2,8 +2,11 @@ FROM alpine:3.18.0
# NOTE: please add packages alphabetically!
RUN apk add --no-cache \
+ bash \
+ bison \
busybox-extras \
chrony \
+ coreutils \
curl \
dhcp-server-vanilla \
dnsmasq \
@@ -12,21 +15,29 @@ RUN apk add --no-cache \
ethtool \
fakeroot \
file \
+ flex \
font-dejavu \
gcc \
git \
graphviz \
+ iperf3 \
iproute2 \
iputils \
jq \
libc-dev \
+ libnet-dev \
+ libnetfilter_conntrack-dev \
+ libnl3-dev \
+ libpcap-dev \
libyang-dev \
linux-headers \
make \
+ ncurses-dev \
nmap \
openssh-client \
openssl \
pandoc-cli \
+ pkgconf \
python3-dev \
qemu-img \
qemu-system-x86_64 \
@@ -37,13 +48,29 @@ RUN apk add --no-cache \
sshpass \
tcpdump \
tshark \
- weasyprint
+ userspace-rcu-dev \
+ weasyprint \
+ zlib-dev
ARG MTOOL_VERSION="3.0"
RUN wget https://github.com/troglobit/mtools/releases/download/v3.0/mtools-$MTOOL_VERSION.tar.gz -O /tmp/mtools-$MTOOL_VERSION.tar.gz
RUN cd /tmp/ && tar zxvf mtools-$MTOOL_VERSION.tar.gz
RUN cd /tmp/mtools-$MTOOL_VERSION && make && make install
+# trafgen and mausezahn, traffic generators for the QoS tests. Alpine
+# has no netsniff-ng package, and mausezahn needs libcli, which is only
+# in edge/testing, so both are built from their release tarballs.
+ARG LIBCLI_VERSION="1.10.7"
+RUN wget https://github.com/dparrish/libcli/archive/refs/tags/V$LIBCLI_VERSION.tar.gz -O /tmp/libcli-$LIBCLI_VERSION.tar.gz
+RUN cd /tmp/ && tar zxf libcli-$LIBCLI_VERSION.tar.gz
+RUN cd /tmp/libcli-$LIBCLI_VERSION && make && make install PREFIX=/usr
+
+ARG NETSNIFF_NG_VERSION="0.6.9"
+RUN wget https://github.com/netsniff-ng/netsniff-ng/archive/refs/tags/v$NETSNIFF_NG_VERSION.tar.gz -O /tmp/netsniff-ng-$NETSNIFF_NG_VERSION.tar.gz
+RUN cd /tmp/ && tar zxf netsniff-ng-$NETSNIFF_NG_VERSION.tar.gz
+RUN cd /tmp/netsniff-ng-$NETSNIFF_NG_VERSION && ./configure --disable-geoip && \
+ make trafgen mausezahn && make trafgen_install mausezahn_install
+
# Alpine's QEMU package does not bundle this for some reason, copied
# from Ubuntu
COPY docker/qemu-ifup /etc
diff --git a/test/docker/pip-requirements.txt b/test/docker/pip-requirements.txt
index 06bb9ebaa..634d921ae 100644
--- a/test/docker/pip-requirements.txt
+++ b/test/docker/pip-requirements.txt
@@ -7,6 +7,10 @@ pydot==1.4.2
pyyaml==6.0.1
passlib==1.7.4
requests~=2.32.4
+# urllib3 2.8.0 normalises the zone id of an IPv6 link-local URL host as a
+# percent-escape, fe80::1%d2a becomes fe80::1%D2a, and the request fails
+# with a name resolution error. Stay on 2.7 until it is fixed upstream.
+urllib3~=2.7.0
pynmea2==1.19.0
# GHSA-cq46-m9x9-j8w2: scapy <=2.6.1 has pickle deserialization vuln in session
# loading (-s flag). Low risk: test framework only uses packet crafting (Ether,
diff --git a/test/infamy/qos.py b/test/infamy/qos.py
new file mode 100644
index 000000000..aedce2f1c
--- /dev/null
+++ b/test/infamy/qos.py
@@ -0,0 +1,387 @@
+"""
+QoS helpers: capabilities, the rendered scheduler, 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 rendered
+
+
+def xpath(port, path=""):
+ return f"/ietf-interfaces:interfaces/interface[name='{port}']/infix-interfaces:qos{path}"
+
+
+def capabilities(target, port):
+ """The port's qos/capabilities container from the operational datastore"""
+ data = target.get_data(xpath(port, "/capabilities"))
+ for iface in data["interfaces"]["interface"]:
+ qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {}
+ return qos.get("capabilities", {})
+ return {}
+
+
+def num_classes(target, port):
+ return capabilities(target, port).get("max-traffic-classes", 8)
+
+
+def offload(target, port):
+ return capabilities(target, port).get("offload", [])
+
+
+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 renders 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(target, ssh, 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"
+ if pmd in supported_pmd_types(target, port):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "ethernet": {"auto-negotiation": {
+ "infix-ethernet-interface:advertised-pmd-types": [pmd]}}
+ }]
+ }
+ }})
+
+ def linked():
+ out = ssh.runsh(f"ip -j link show {port}").stdout
+ link = json.loads(out or "[]")
+ return link and "LOWER_UP" in link[0].get("flags", []) and scheduler(ssh, port)
+
+ until(linked, attempts=60)
+ print(ssh.runsh(f"ethtool {port} | grep -i speed").stdout.strip())
+ return 100_000_000
+
+ if not supported_pmd_types(target, port):
+ target.put_config_dicts({"ietf-interfaces": {
+ "interfaces": {
+ "interface": [{
+ "name": port,
+ "infix-interfaces:qos": {"egress": {"rate-limit": {"rate": 10_000_000}}}
+ }]
+ }
+ }})
+ until(lambda: (root_qdisc(ssh, port) or {}).get("kind") == "tbf")
+ show_shaper(ssh, 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(target, ssh, port, dsa):
+ """Log what the fabric took after a scheduler change: the offload list
+ and the switch driver's recent messages. On a switch port the
+ scheduler must be offloaded, or the measurement is meaningless"""
+ if dsa:
+ until(lambda: "transmission-selection" in offload(target, port))
+ print(f"{port} offload: {offload(target, port)}")
+ log = ssh.runsh("sudo dmesg | grep -i 'mv88e6xxx\\|dsa' | tail -5").stdout.strip()
+ if log:
+ print(log)
+
+
+def show_shaper(ssh, port):
+ """Log what the rate limit became: the root qdisc with its offloaded
+ flag, and on a switch port the port registers, where a shaper the
+ driver took shows up as the egress rate control words"""
+ print(json.dumps(root_qdisc(ssh, port)))
+ print(ssh.runsh(f"ethtool {port} | grep -i speed").stdout.strip())
+ if "DEVTYPE=dsa" in ssh.runsh(f"cat /sys/class/net/{port}/uevent").stdout.split():
+ regs = ssh.runsh(f"sudo ethtool -d {port}").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 ad004a8dc..53fab3179 100644
--- a/test/infamy/restconf.py
+++ b/test/infamy/restconf.py
@@ -66,6 +66,9 @@ def requests_workaround(method, url, json, headers, auth, verify=False, retry=0)
response = requests_workaround(method, url, json, headers, auth,
verify, retry)
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