From 7af6cf18598b0e722f56c21c332830b93faa989d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 15:30:15 +0200 Subject: [PATCH 01/21] confd: update ieee802-dot1q-types to revision 2023-10-26 The new upcoming QoS model needs num-traffic-class-type, added to ieee802-dot1q-types by IEEE Std 802.1Qcw-2023. Signed-off-by: Joachim Wiberg --- src/confd/yang/confd.inc | 2 +- ...ng => ieee802-dot1q-types@2023-10-26.yang} | 557 ++++++++++++------ 2 files changed, 364 insertions(+), 195 deletions(-) rename src/confd/yang/confd/{ieee802-dot1q-types@2022-10-29.yang => ieee802-dot1q-types@2023-10-26.yang} (60%) diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 62ba37b9d..66fd7da89 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" 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"; + } } } } From 40efc76e00c4e090e905431798f185a42c42c7d0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 15:31:54 +0200 Subject: [PATCH 02/21] board: enable ETS qdisc and sync x86_64 QoS kernel options Per-interface transmission selection is rendered as tc mqprio when the driver offloads it and tc ets otherwise. Signed-off-by: Joachim Wiberg --- board/aarch64/linux_defconfig | 8 ++++++++ board/arm/linux_defconfig | 8 ++++++++ board/riscv64/linux_defconfig | 8 ++++++++ board/x86_64/linux_defconfig | 13 +++++++++++++ 4 files changed, 37 insertions(+) diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index b7c4fe051..d9a37bd96 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -245,13 +245,21 @@ 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_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/arm/linux_defconfig b/board/arm/linux_defconfig index 3c0639ad7..79a31811c 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -229,13 +229,21 @@ 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_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/riscv64/linux_defconfig b/board/riscv64/linux_defconfig index 080eca13e..8cf6abcaf 100644 --- a/board/riscv64/linux_defconfig +++ b/board/riscv64/linux_defconfig @@ -193,13 +193,21 @@ 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_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..7916d6c69 100644 --- a/board/x86_64/linux_defconfig +++ b/board/x86_64/linux_defconfig @@ -219,9 +219,22 @@ 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_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 From c3865acbe04f489917552974a9b38a6c06f35bee Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 15:34:04 +0200 Subject: [PATCH 03/21] confd: initial QoS support, classification, traffic classes Add a qos container to every interface, with names and types from IEEE 802.1Q-2022 wherever the standard defines the concept. The defaults are a complete pipeline, trust DSCP then PCP and one traffic class per transmit queue per Table 8-5, rendered on every physical port from boot, which retires the 25-mqprio script. Ingress classification is programmed through the kernel's DCB APP table and apptrust list. Few drivers have those operations, so when the calls fail the same maps are rendered as 'tc flower' rules on the port's ingress, in trust order, setting the skb priority; on a NIC or SoC MAC that covers all traffic, on a switch the CPU path. The orders a driver accepts cannot be queried, so a table keyed by driver name reports them as a capability. The traffic class table is programmed as 'tc mqprio' with 'hw 1', else as 'tc ets' in the kernel, which numbers bands the opposite way so the map, quanta and strict count invert. A single-queue port has no queue structure to respect and gets the kernel's eight classes; the root qdisc tells where it ended up and is reported as the offload capability. Strict classes must be the highest-numbered ones, the only layout ets can express. Egress remarking uses the same DCB table via 'dcb rewr'; without it DSCP is rewritten with 'tc pedit' on egress, PCP has no software counterpart since act_vlan cannot change the priority alone. Policing and storm control are omitted for now. Signed-off-by: Joachim Wiberg --- .../rootfs/usr/libexec/infix/init.d/25-mqprio | 60 -- src/confd/src/Makefile.am | 1 + src/confd/src/if-qos.c | 776 ++++++++++++++++++ src/confd/src/interfaces.c | 4 + src/confd/src/interfaces.h | 4 + src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-if-qos.yang | 492 +++++++++++ .../yang/confd/infix-if-qos@2026-09-07.yang | 1 + src/confd/yang/confd/infix-interfaces.yang | 8 + ....yang => infix-interfaces@2026-09-07.yang} | 0 .../python/yanger/ietf_interfaces/link.py | 66 +- 11 files changed, 1351 insertions(+), 63 deletions(-) delete mode 100755 board/common/rootfs/usr/libexec/infix/init.d/25-mqprio create mode 100644 src/confd/src/if-qos.c create mode 100644 src/confd/yang/confd/infix-if-qos.yang create mode 120000 src/confd/yang/confd/infix-if-qos@2026-09-07.yang rename src/confd/yang/confd/{infix-interfaces@2026-06-11.yang => infix-interfaces@2026-09-07.yang} (100%) 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/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..ee6d6adf7 --- /dev/null +++ b/src/confd/src/if-qos.c @@ -0,0 +1,776 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#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 */ +}; + +/* + * 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[4]; +} dcb_drivers[] = { + { "sparx5-switch", { "pcp", "dscp", "dscp-pcp", NULL } }, + { "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL } }, +}; + +/* + * 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; +} + +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; +} + +/* 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); + } + + 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); + /* One code point per priority, a DEI 1 entry would replace the DEI 0 one */ + if (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); + } + } + + if (dscp && !strcmp(dscp, "from-priority")) { + fputs("if [ $rewr_err -ne 0 ]; then\n", fp); + gen_remark_pedit(fp, ifname); + 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 mqprio takes the 802.1Q map as-is and offloads the class layout to + * the driver. tc ets is the software rendering: 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) +{ + int i, nstrict = 0; + + fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname); + if (eg->num_tc < 2) + return; + + if (!iface_has_quirk(ifname, "broken-mqprio")) { + fprintf(fp, "tc qdisc add dev %s root mqprio num_tc %d map", ifname, eg->num_tc); + for (i = 0; i < NUM_PRIO; i++) + fprintf(fp, " %d", eg->map[i]); + fputs(" queues", fp); + for (i = 0; i < eg->num_tc; i++) + fprintf(fp, " 1@%d", i); + fputs(" hw 1 2>/dev/null ||\n", fp); + } + + for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--) + nstrict++; + + fprintf(fp, "tc qdisc add dev %s root ets bands %d strict %d", ifname, 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 prio-pcp prio-dscp 2>/dev/null\n", ifname); + 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))) + 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 66fd7da89..7be18892a 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -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" "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/infix-if-qos.yang b/src/confd/yang/confd/infix-if-qos.yang new file mode 100644 index 000000000..47e34c774 --- /dev/null +++ b/src/confd/yang/confd/infix-if-qos.yang @@ -0,0 +1,492 @@ +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 + */ + + 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."; + } + } + 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."; + } + } + } + } + } +} 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..7d3ce41ea 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -140,15 +140,72 @@ 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"], +} + + +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. The root qdisc is only + # ever mqprio with hw offload, which the kernel refuses without driver + # support, so its presence means the driver schedules the classes; + # ets means the kernel does. mqprio never sets the offloaded flag. + 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, []): + if qdisc.get("root") and qdisc.get("kind") == "mqprio": + offload.append("transmission-selection") + + 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 +258,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 +274,6 @@ def interfaces(ifname=None): ipaddr = addrs.get(ifname, {}) - interfaces.append(interface(iplink, ipaddr, systemjson)) + interfaces.append(interface(iplink, ipaddr, systemjson, qdiscs)) return interfaces From 3c781f23ff942516dd3b7c550edc3c5806fd438e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 8 Sep 2026 07:45:45 +0200 Subject: [PATCH 04/21] test: infamy: print the RESTCONF error body on HTTP failures A 4xx or 5xx from rousette carries the offending node and reason in the response body, which the exception message drops. Print it before raising. Signed-off-by: Joachim Wiberg --- test/infamy/restconf.py | 3 +++ 1 file changed, 3 insertions(+) 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 From c0d613e8fd1215f13dd9badadf3e2ae8241291a1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 15:48:17 +0200 Subject: [PATCH 05/21] test: add QoS test suite The QoS model has no test coverage. The tests run on any target: the software rendering is verified where mqprio and DCB are not offloaded, the hardware tables where they are, and DSCP classification end to end through a routed VLAN interface. Signed-off-by: Joachim Wiberg --- test/case/interfaces/Readme.adoc | 22 +- test/case/interfaces/all.yaml | 3 + test/case/interfaces/qos.yaml | 15 ++ .../interfaces/qos_classify_dscp/Readme.adoc | 1 + .../interfaces/qos_classify_dscp/test.adoc | 44 ++++ .../case/interfaces/qos_classify_dscp/test.py | 147 +++++++++++++ .../interfaces/qos_classify_dscp/topology.dot | 1 + .../interfaces/qos_classify_dscp/topology.svg | 51 +++++ .../qos_factory_default/Readme.adoc | 1 + .../interfaces/qos_factory_default/test.adoc | 32 +++ .../interfaces/qos_factory_default/test.py | 106 ++++++++++ .../qos_factory_default/topology.dot | 1 + .../qos_factory_default/topology.svg | 42 ++++ .../qos_ingress_classify/Readme.adoc | 1 + .../interfaces/qos_ingress_classify/test.adoc | 42 ++++ .../interfaces/qos_ingress_classify/test.py | 194 ++++++++++++++++++ .../qos_ingress_classify/topology.dot | 1 + .../qos_ingress_classify/topology.svg | 42 ++++ .../interfaces/qos_traffic_class/Readme.adoc | 1 + .../interfaces/qos_traffic_class/test.adoc | 34 +++ .../case/interfaces/qos_traffic_class/test.py | 146 +++++++++++++ .../interfaces/qos_traffic_class/topology.dot | 1 + .../interfaces/qos_traffic_class/topology.svg | 42 ++++ .../interfaces/qos_validation/Readme.adoc | 1 + test/case/interfaces/qos_validation/test.adoc | 34 +++ test/case/interfaces/qos_validation/test.py | 115 +++++++++++ .../interfaces/qos_validation/topology.dot | 1 + .../interfaces/qos_validation/topology.svg | 42 ++++ 28 files changed, 1162 insertions(+), 1 deletion(-) create mode 100644 test/case/interfaces/qos.yaml create mode 120000 test/case/interfaces/qos_classify_dscp/Readme.adoc create mode 100644 test/case/interfaces/qos_classify_dscp/test.adoc create mode 100755 test/case/interfaces/qos_classify_dscp/test.py create mode 120000 test/case/interfaces/qos_classify_dscp/topology.dot create mode 100644 test/case/interfaces/qos_classify_dscp/topology.svg create mode 120000 test/case/interfaces/qos_factory_default/Readme.adoc create mode 100644 test/case/interfaces/qos_factory_default/test.adoc create mode 100755 test/case/interfaces/qos_factory_default/test.py create mode 120000 test/case/interfaces/qos_factory_default/topology.dot create mode 100644 test/case/interfaces/qos_factory_default/topology.svg create mode 120000 test/case/interfaces/qos_ingress_classify/Readme.adoc create mode 100644 test/case/interfaces/qos_ingress_classify/test.adoc create mode 100755 test/case/interfaces/qos_ingress_classify/test.py create mode 120000 test/case/interfaces/qos_ingress_classify/topology.dot create mode 100644 test/case/interfaces/qos_ingress_classify/topology.svg create mode 120000 test/case/interfaces/qos_traffic_class/Readme.adoc create mode 100644 test/case/interfaces/qos_traffic_class/test.adoc create mode 100755 test/case/interfaces/qos_traffic_class/test.py create mode 120000 test/case/interfaces/qos_traffic_class/topology.dot create mode 100644 test/case/interfaces/qos_traffic_class/topology.svg create mode 120000 test/case/interfaces/qos_validation/Readme.adoc create mode 100644 test/case/interfaces/qos_validation/test.adoc create mode 100755 test/case/interfaces/qos_validation/test.py create mode 120000 test/case/interfaces/qos_validation/topology.dot create mode 100644 test/case/interfaces/qos_validation/topology.svg 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..ea3fbeb94 --- /dev/null +++ b/test/case/interfaces/qos.yaml @@ -0,0 +1,15 @@ +--- +- 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 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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + 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..ee978d401 --- /dev/null +++ b/test/case/interfaces/qos_factory_default/test.py @@ -0,0 +1,106 @@ +#!/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"] == "mqprio": + return opts.get("map", [])[:8] == TABLE_8_5[num_tc] + 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 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + 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 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + 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..35f8292c5 --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/test.adoc @@ -0,0 +1,34 @@ +=== 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. A driver with mqprio offload shows an offloaded +mqprio with the same map; otherwise the kernel runs 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. + +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. 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 +. 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..10659f77e --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/test.py @@ -0,0 +1,146 @@ +#!/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. A driver with mqprio offload shows an offloaded +mqprio with the same map; otherwise the kernel runs 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. + +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. 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"] == "mqprio": + return opts.get("map", [])[:8] == prio_map + 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"): + kind = root_qdisc(tgtssh, port)["kind"] + until(lambda: ("transmission-selection" in capabilities(target, port).get("offload", [])) + == (kind == "mqprio")) + + 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("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 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + 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 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + From 84cfa9e52ea96880b192d5f75a68058a183faa19 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 15:48:22 +0200 Subject: [PATCH 06/21] doc: describe per-interface QoS configuration Update QoS page, which so far has covered only the Marvell hardware defaults and the hard-coded VLAN PCP mapping. Rewrite it around the new infix-if-qos.yang model, with the IEEE 802.1Q Table 8-5 factory default, the Table 34-1 layout for reserved streams and what offload capabilities mean on hardware with and without DCB support. Signed-off-by: Joachim Wiberg --- board/aarch64/microchip-ev23x71a/README.md | 4 +- doc/ChangeLog.md | 8 + doc/img/qos-pipeline.svg | 4 + doc/qos.md | 548 ++++++++++++++++++--- 4 files changed, 494 insertions(+), 70 deletions(-) create mode 100644 doc/img/qos-pipeline.svg 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/doc/ChangeLog.md b/doc/ChangeLog.md index d6d9fa15a..ca1f371af 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -16,6 +16,14 @@ 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) ### Added 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 @@ + + + +
Forwarding
Bridge FDB lookup and MAC learning, VLAN membership, STP state, ACLs, multicast replication.
→ egress port(s)
INGRESS receiving port
EGRESS transmitting port
1 Classify
Trust order picks PCP or DSCP, else the port default.
→ priority + drop precedence
2 Match flows
TCAM match on a 5-tuple may override the priority and drop precedence.
3 Police and gate
Policers, storm control,
PSFP (IEEE 802.1Qci)
4 Map to traffic class and queue
IEEE 802.1Q 8.6.6. The egress port’s table decides
5 Transmission selection
IEEE 802.1Q 8.6.8. TAS gates,
then strict priority, ETS or CBS
6 Egress processing
Remark PCP, DEI and DSCP
Port cap
frame
wire
\ No newline at end of file diff --git a/doc/qos.md b/doc/qos.md index d937104f6..2202095ce 100644 --- a/doc/qos.md +++ b/doc/qos.md @@ -2,93 +2,469 @@ 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 + +![QoS pipeline](img/qos-pipeline.svg){ 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
+
+ + +### 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: + +
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: + +
admin@example:/config/> edit interface e1 qos egress traffic-class-table
+admin@example:/config/…/traffic-class-table/> set preset ieee-sr
+admin@example:/config/…/traffic-class-table/> leave
+
+ + +## 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` +and `transmission-selection` 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 | +| Traffic class table | `tc mqprio` | `ndo_setup_tc` | `tc ets`, 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 | +| Data-center NICs[^12] | software[^14] | DSCP, software| hardware | +| DSA `felix`, `ksz` | software[^14] | DSCP, software| hardware | +| DSA `mv88e6xxx`, Marvell LinkStreet | software | 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, see [Marvell LinkStreet](#marvell-linkstreet) +below. 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 driver has no DCB support, so ingress classification +is fixed by the hardware defaults below, while the traffic class table +_is_ offloaded. This section is _only_ valid for generations with 8 +output queues per port. -![Hardware offloading for Marvell Link Street](img/qos-hw-mvls.svg) +![Marvell LinkStreet offloading](img/qos-hw-mvls.svg){ 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 +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: +Both PCP and DSCP 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 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 | +| 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 | +/// table-caption +Marvell LinkStreet default PCP and DSCP to queue mapping and WRR weights. +/// -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. +For packets containing both a VLAN tag and an IP header, PCP takes +precedence over DSCP. In cases where neither is 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 +(WRR)[^11] 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 +queue is roughly equivalent to the percentage of the available bandwidth +reserved for it. 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. - -## Software Forwarded Traffic +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 the DSCP. 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. -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. -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 -![Ingress and Egress Priority mapping for VLAN interfaces](img/qos-vlan-iface.svg) +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. +![VLAN priority mapping](img/qos-vlan-iface.svg){ 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.
admin@example:/config/> edit interface e1.10
@@ -100,15 +476,49 @@ admin@example:/config/interface/e1.20/> leave
 admin@example:/>
 
-## 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. + +![Hardware and software QoS](img/qos-complex.svg){ width=600 } +/// figure-caption +Hardware and software QoS handling. +/// -![Hardware and Software QoS Handling](img/qos-complex.svg) +[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 From 9d8a3485e61f1fb23038235cff12e9583319caf1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 17:57:29 +0200 Subject: [PATCH 07/21] doc: center tables and fit header cells to their content Figures are centered on the page, tables were left-aligned. The old .center-table class was never used; a rule on Material's table wrapper centers every table without markup. Material also gives every header cell a 5rem minimum width, so a nine column table of single digits overflows the page. Let headers size to their content. Signed-off-by: Joachim Wiberg --- doc/extra.css | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/extra.css b/doc/extra.css index 3e9c836df..634027264 100644 --- a/doc/extra.css +++ b/doc/extra.css @@ -102,14 +102,15 @@ 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; } /* CLI terminal output styling */ From 6a5f7ca7cc5ce7a42f5ebd7c364d61a8b5a363f7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 7 Sep 2026 18:07:31 +0200 Subject: [PATCH 08/21] doc: tighten code block line height Material's 1.4 line height leaves visible gaps between box-drawing characters in CLI output and diagrams. Signed-off-by: Joachim Wiberg --- doc/extra.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/extra.css b/doc/extra.css index 634027264..7df85d675 100644 --- a/doc/extra.css +++ b/doc/extra.css @@ -113,6 +113,12 @@ body { 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 */ .md-typeset pre.cli .title { font-weight: bold; From 99bff76d14767ca791d727943b73cc2fbc6c1f44 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 18:54:30 +0200 Subject: [PATCH 09/21] linux: offload QoS classification and remarking on Marvell LinkStreet The mv88e6xxx driver has no DCB ops, so on 88E6390 and 88E6393X boards the trust order, priority maps and remarking only reached frames that passed the CPU. Frames the switch forwarded port to port were classified by the chip defaults and never remarked. Add DSA support for the PCP APP selector and the DCB rewrite table, and mv88e6xxx ops for the per-port PCP and DSCP tables, InitialPri/TagIfBoth as trust order, the default priority, and the egress PCP and DSCP tables. The mqprio map is pushed into the tables as queue priority, so the traffic class table applies to forwarded frames too. The scheduler stays at the chip default WRR. Frames the CPU sends bypass the switch tables, so their DSCP is still remarked by the kernel on DSA ports. Frames the CPU receives carried a stale priority: tag_dsa read the DSA tag after stripping it, picking up a byte of the source MAC address instead, which only showed once the software classifier no longer overwrote it. The renderer and the operational datastore now treat the driver as DCB capable with all four trust orders: dcb apptrust set dev e1 order dscp pcp dcb app add dev e1 pcp-prio 5nd:5 dscp-prio 46:5 default-prio 2 dcb rewr add dev e2 prio-pcp 5:5nd prio-dscp 5:40 Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 3 + doc/qos.md | 100 ++- ...-default-priority-init-on-unsupporte.patch | 45 ++ ...net-dsa-Support-the-PCP-APP-selector.patch | 172 ++++ ...net-dsa-Support-DCB-priority-rewrite.patch | 259 ++++++ ...6xxx-Add-DCB-priority-classification.patch | 736 ++++++++++++++++++ ...a-mv88e6xxx-Add-DCB-priority-rewrite.patch | 433 +++++++++++ ...e6xxx-Queue-frames-by-the-mqprio-map.patch | 398 ++++++++++ ...Read-the-tag-priority-before-the-hea.patch | 59 ++ src/confd/src/if-qos.c | 65 +- .../python/yanger/ietf_interfaces/link.py | 1 + 11 files changed, 2222 insertions(+), 49 deletions(-) create mode 100644 patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch create mode 100644 patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch create mode 100644 patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch create mode 100644 patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch create mode 100644 patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch create mode 100644 patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch create mode 100644 patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index ca1f371af..e6158334d 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -24,6 +24,9 @@ All notable changes to the project are documented in this file. 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) +- Marvell LinkStreet 88E6390 and 88E6393X switch ports offload QoS + classification, trust order and remarking to the switch, so they apply + to hardware forwarded traffic as well ### Added diff --git a/doc/qos.md b/doc/qos.md index 2202095ce..cc6c36420 100644 --- a/doc/qos.md +++ b/doc/qos.md @@ -261,7 +261,11 @@ Configured under `qos egress remark`, both leaves default to `none`: 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: +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
@@ -381,9 +385,9 @@ 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        |
 | Data-center NICs[^12]               | software[^14]  | DSCP, software| hardware        |
 | DSA `felix`, `ksz`                  | software[^14]  | DSCP, software| hardware        |
-| DSA `mv88e6xxx`, Marvell LinkStreet | software       | DSCP, software| hardware        |
 | Other NICs and SoC MACs[^13]        | software       | DSCP, software| software        |
 /// table-caption
 QoS support per driver family.
@@ -391,19 +395,19 @@ 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, see [Marvell LinkStreet](#marvell-linkstreet)
-below.  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/`.
+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 switch chips is managed by the `mv88e6xxx` driver in the
-Linux kernel.  The driver has no DCB support, so ingress classification
-is fixed by the hardware defaults below, while the traffic class table
-_is_ offloaded.  This section is _only_ valid for generations with 8
-output queues per port.
+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.
 
 ![Marvell LinkStreet offloading](img/qos-hw-mvls.svg){ width=600 }
 /// figure-caption
@@ -415,40 +419,47 @@ ingress, here interface _e1_ and _e3_.  In this example, both packets
 are forwarded to the same outgoing interface (_e2_), subject to output
 queueing.
 
-Both PCP and DSCP 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 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 |
+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` applies to hardware forwarded frames as well:
+each priority is queued in the first queue of its traffic class.  The
+class algorithms and weights are not offloaded, however.  The switch
+serves its eight queues by the fixed Weighted Round Robin (WRR)[^11]
+weights below, whatever the `traffic-class` list says, for frames the
+CPU sends as well as for forwarded ones.
+
+| Queue | Weight |
+|------:|-------:|
+|     0 |      1 |
+|     1 |      2 |
+|     2 |      3 |
+|     3 |      6 |
+|     4 |     12 |
+|     5 |     17 |
+|     6 |     25 |
+|     7 |     33 |
 /// table-caption
-Marvell LinkStreet default PCP and DSCP to queue mapping and WRR weights.
+Marvell LinkStreet WRR weights per output queue.
 ///
 
-For packets containing both a VLAN tag and an IP header, PCP takes
-precedence over DSCP.  In cases where neither is available, packets are
-always assigned to queue 0.
-
-Each port's set of 8 egress queues operate on a Weighted Round Robin
-(WRR)[^11] 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.
-
-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 the DSCP.  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.
+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.
 
 
 ## VLAN Interfaces
@@ -522,3 +533,10 @@ Hardware and software QoS handling.
        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..8b2252a18
--- /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/75] 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..f4e0a7aa7
--- /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/75] 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..daebcfcab
--- /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/75] 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..24efba0d8
--- /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/75] 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..e5fe77df6
--- /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/75] 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..4746da936
--- /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/75] 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..0ca63e304
--- /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 140ad7beeb39d9d7ad8e0e4982a68f501e4dee70 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg 
+Date: Fri, 11 Sep 2026 15:54:16 +0200
+Subject: [PATCH 75/75] 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/src/confd/src/if-qos.c b/src/confd/src/if-qos.c
index ee6d6adf7..057710797 100644
--- a/src/confd/src/if-qos.c
+++ b/src/confd/src/if-qos.c
@@ -109,10 +109,12 @@ static bool qos_is_explicit(struct lyd_node *node)
  */
 static const struct {
 	const char *driver;
-	const char *orders[4];
+	const char *orders[5];
+	bool pcp_encoded;	/* fabric always encodes PCP from the priority */
 } dcb_drivers[] = {
-	{ "sparx5-switch",  { "pcp", "dscp", "dscp-pcp", NULL } },
-	{ "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL } },
+	{ "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 },
 };
 
 /*
@@ -149,6 +151,26 @@ static bool qos_is_port(const char *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];
@@ -186,6 +208,20 @@ static int qos_dcb_driver(const char *ifname)
 	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)
 {
@@ -568,9 +604,12 @@ static void gen_remark(FILE *fp, const char *ifname, struct lyd_node *remark)
 	if (qos_dcb_driver(ifname) < 0) {
 		fputs("rewr_err=1\n", fp);
 	} else {
-		fprintf(fp, "dcb rewr flush dev %s prio-pcp prio-dscp 2>/dev/null || rewr_err=1\n", ifname);
+		bool encoded = qos_pcp_encoded(ifname);
+
+		fprintf(fp, "dcb rewr flush dev %s%s prio-dscp 2>/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 (pcp && !strcmp(pcp, "from-priority")) {
+		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);
@@ -584,10 +623,19 @@ static void gen_remark(FILE *fp, const char *ifname, struct lyd_node *remark)
 		}
 	}
 
+	/*
+	 * 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")) {
-		fputs("if [ $rewr_err -ne 0 ]; then\n", fp);
+		bool dsa = qos_is_dsa(ifname);
+
+		if (!dsa)
+			fputs("if [ $rewr_err -ne 0 ]; then\n", fp);
 		gen_remark_pedit(fp, ifname);
-		fputs("fi\n", fp);
+		if (!dsa)
+			fputs("fi\n", fp);
 	}
 }
 
@@ -671,7 +719,8 @@ static int gen_reset(struct dagger *net, const char *ifname)
 	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 prio-pcp prio-dscp 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);
diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py
index 7d3ce41ea..6e5137c01 100644
--- a/src/statd/python/yanger/ietf_interfaces/link.py
+++ b/src/statd/python/yanger/ietf_interfaces/link.py
@@ -145,6 +145,7 @@ def ptp_capabilities(ifname, systemjson):
 QOS_TRUST_ORDERS = {
     "sparx5-switch":  ["pcp", "dscp", "dscp-pcp"],
     "lan966x-switch": ["pcp", "dscp", "dscp-pcp"],
+    "mv88e6085":      ["pcp", "dscp", "pcp-dscp", "dscp-pcp"],
 }
 
 

From 8fba71385f90f9dc3dbb4a97c0c4f4e704c347c1 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg 
Date: Wed, 9 Sep 2026 18:54:31 +0200
Subject: [PATCH 10/21] test: add QoS bridge classification test

Bridge two ports and send frames through the switch, so on a switch chip
they never pass the CPU and only the hardware tables can classify and
remark them.  The DSCP and PCP on the wire at the egress port reveal the
priority the ingress port assigned, with trust dscp on untagged frames,
trust pcp with a custom map on tagged frames, and remarking off.

PCP is only checked where the driver offloads remarking, there is no
software path for it.  Skipped on a switch port without DCB support,
since forwarded frames never see the kernel's rules there.

Signed-off-by: Joachim Wiberg 
---
 test/case/interfaces/qos.yaml                 |   3 +
 .../qos_bridge_classify/Readme.adoc           |   1 +
 .../interfaces/qos_bridge_classify/test.adoc  |  44 ++++
 .../interfaces/qos_bridge_classify/test.py    | 229 ++++++++++++++++++
 .../qos_bridge_classify/topology.dot          |  24 ++
 .../qos_bridge_classify/topology.svg          |  51 ++++
 6 files changed, 352 insertions(+)
 create mode 120000 test/case/interfaces/qos_bridge_classify/Readme.adoc
 create mode 100644 test/case/interfaces/qos_bridge_classify/test.adoc
 create mode 100755 test/case/interfaces/qos_bridge_classify/test.py
 create mode 100644 test/case/interfaces/qos_bridge_classify/topology.dot
 create mode 100644 test/case/interfaces/qos_bridge_classify/topology.svg

diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml
index ea3fbeb94..4e87ce767 100644
--- a/test/case/interfaces/qos.yaml
+++ b/test/case/interfaces/qos.yaml
@@ -13,3 +13,6 @@
 
 - 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
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 @@
+
+
+
+
+
+
+1x3
+
+
+
+host
+
+host
+
+mgmt
+
+data1
+
+data2
+
+
+
+target
+
+mgmt
+
+data1
+
+data2
+
+target
+
+
+
+host:mgmt--target:mgmt
+
+
+
+
+host:data1--target:data1
+
+
+
+
+host:data2--target:data2
+
+
+
+

From 58ea0c3578d82d7b62cdd73a4fb8702340a04d83 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg 
Date: Wed, 16 Sep 2026 21:47:41 +0200
Subject: [PATCH 11/21] confd: prefer the ets scheduler for transmission
 selection

Transmission selection was rendered as 'tc mqprio' with 'hw 1' on every
port, falling back to 'tc ets' only when the driver refused it.  mqprio
carries the priority to traffic class map and nothing else, so a fabric
that offloads both, like Microchip sparx5 and lan966x, was given the map
and never the class algorithms or weights.  mqprio also never marks
itself offloaded, which left the operational data guessing from its
presence.

Render 'tc ets' everywhere.  It carries the whole of transmission
selection, drivers that offload it report so in the qdisc, and the
software scheduler is the same qdisc.  Marvell LinkStreet, which
offloads the map through mqprio and nothing through ets, keeps mqprio
until its driver learns ets.

Signed-off-by: Joachim Wiberg 
---
 doc/ChangeLog.md                              |  3 +++
 doc/qos.md                                    |  2 +-
 src/confd/src/if-qos.c                        | 22 +++++++++++++++----
 .../python/yanger/ietf_interfaces/link.py     | 11 +++++-----
 4 files changed, 28 insertions(+), 10 deletions(-)

diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index e6158334d..9620bdb3c 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -24,6 +24,9 @@ All notable changes to the project are documented in this file.
   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)
+- 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 and remarking to the switch, so they apply
   to hardware forwarded traffic as well
diff --git a/doc/qos.md b/doc/qos.md
index cc6c36420..89e86e10a 100644
--- a/doc/qos.md
+++ b/doc/qos.md
@@ -366,7 +366,7 @@ hardware depends on the driver implementing the matching hook:
 | 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  |
-| Traffic class table    | `tc mqprio`     | `ndo_setup_tc`      | `tc ets`, software   |
+| Transmission selection | `tc ets`        | `TC_SETUP_QDISC_ETS`| software             |
 /// table-caption
 QoS features and their Linux backends.
 ///
diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c
index 057710797..9d278185e 100644
--- a/src/confd/src/if-qos.c
+++ b/src/confd/src/if-qos.c
@@ -111,10 +111,11 @@ static const struct {
 	const char *driver;
 	const char *orders[5];
 	bool pcp_encoded;	/* fabric always encodes PCP from the priority */
+	bool mqprio;		/* offloads the class map through mqprio only */
 } 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 },
+	{ "sparx5-switch",  { "pcp", "dscp", "dscp-pcp", NULL }, false, false },
+	{ "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false, false },
+	{ "mv88e6085",      { "pcp", "dscp", "pcp-dscp", "dscp-pcp", NULL }, true, true },
 };
 
 /*
@@ -222,6 +223,19 @@ static bool qos_pcp_encoded(const char *ifname)
 	return i >= 0 && dcb_drivers[i].pcp_encoded;
 }
 
+/*
+ * ets carries the whole of transmission selection, and a driver that
+ * offloads it says so in the qdisc, so it is what every port gets.
+ * The one fabric that offloads the class map through mqprio, and
+ * nothing through ets, keeps mqprio until its driver learns ets.
+ */
+static bool qos_mqprio(const char *ifname)
+{
+	int i = qos_dcb_driver(ifname);
+
+	return i >= 0 && dcb_drivers[i].mqprio;
+}
+
 /* Unknown drivers are not limited: without DCB the order is honoured in software. */
 static bool qos_trust_supported(const char *ifname, const char *order)
 {
@@ -683,7 +697,7 @@ static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg)
 	if (eg->num_tc < 2)
 		return;
 
-	if (!iface_has_quirk(ifname, "broken-mqprio")) {
+	if (qos_mqprio(ifname) && !iface_has_quirk(ifname, "broken-mqprio")) {
 		fprintf(fp, "tc qdisc add dev %s root mqprio num_tc %d map", ifname, eg->num_tc);
 		for (i = 0; i < NUM_PRIO; i++)
 			fprintf(fp, " %d", eg->map[i]);
diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py
index 6e5137c01..451b3ed90 100644
--- a/src/statd/python/yanger/ietf_interfaces/link.py
+++ b/src/statd/python/yanger/ietf_interfaces/link.py
@@ -170,10 +170,10 @@ def qos_capabilities(iplink, qdiscs):
         result["supported-trust-order"] = trust
 
     # Stages the driver runs.  DCB tables exist only on drivers with the
-    # operations, so only those ports are asked.  The root qdisc is only
-    # ever mqprio with hw offload, which the kernel refuses without driver
-    # support, so its presence means the driver schedules the classes;
-    # ets means the kernel does.  mqprio never sets the offloaded flag.
+    # operations, so only those ports are asked.  ets reports offloaded
+    # when the driver took it.  mqprio never sets that flag,
+    # but it is only ever installed with hw 1, which the kernel refuses
+    # without driver support, so its presence means the same.
     offload = []
     if trust:
         app = HOST.run_json(["dcb", "-j", "app", "show", "dev", ifname], {})
@@ -184,7 +184,8 @@ def qos_capabilities(iplink, qdiscs):
             offload.append("remarking")
 
     for qdisc in qdiscs.get(ifname, []):
-        if qdisc.get("root") and qdisc.get("kind") == "mqprio":
+        kind, offloaded = qdisc.get("kind"), qdisc.get("offloaded", False)
+        if kind == "mqprio" and qdisc.get("root") or kind == "ets" and offloaded:
             offload.append("transmission-selection")
 
     if offload:

From af02ba03d82392500bda75a33efe1c4d2f76dabf Mon Sep 17 00:00:00 2001
From: Joachim Wiberg 
Date: Wed, 16 Sep 2026 21:47:41 +0200
Subject: [PATCH 12/21] confd: add a per-port egress rate limit

Nothing in the model could hold a port below its link speed, which a
virtual port has none of, so there was no way to put the traffic
classes under load there and see the scheduler work.  The design has
the node for it, one token bucket on the whole port.

Add /interfaces/interface/qos/egress/rate-limit with a rate in bits per
second and an optional burst, rendered as a 'tc tbf' root with the ets
scheduler beneath it:

    tc qdisc add dev e1 root handle 1: tbf rate 10000000bit burst 12500 latency 100ms
    tc qdisc add dev e1 parent 1:1 handle 2: ets bands 8 strict 8 priomap ...

An unset burst covers ten milliseconds at rate, never less than one
frame.  mqprio can only be the root, so a rate limited port schedules
with ets.  The operational offload list gains 'rate-limit', reported
when the driver marks the tbf offloaded.  NET_SCH_TBF is enabled on all
boards.

Signed-off-by: Joachim Wiberg 
---
 board/aarch64/linux_defconfig                 |  1 +
 board/arm/linux_defconfig                     |  1 +
 board/riscv64/linux_defconfig                 |  1 +
 board/x86_64/linux_defconfig                  |  1 +
 doc/ChangeLog.md                              |  3 ++
 doc/qos.md                                    | 29 +++++++++++++-
 src/confd/src/if-qos.c                        | 35 ++++++++++++++--
 src/confd/yang/confd.inc                      |  2 +-
 src/confd/yang/confd/infix-if-qos.yang        | 40 +++++++++++++++++++
 .../python/yanger/ietf_interfaces/link.py     |  6 ++-
 .../interfaces/qos_traffic_class/test.adoc    | 10 +++--
 .../case/interfaces/qos_traffic_class/test.py | 34 ++++++++++++++--
 12 files changed, 149 insertions(+), 14 deletions(-)

diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig
index d9a37bd96..c23012847 100644
--- a/board/aarch64/linux_defconfig
+++ b/board/aarch64/linux_defconfig
@@ -246,6 +246,7 @@ 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
diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig
index 79a31811c..1ed746af3 100644
--- a/board/arm/linux_defconfig
+++ b/board/arm/linux_defconfig
@@ -230,6 +230,7 @@ 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
diff --git a/board/riscv64/linux_defconfig b/board/riscv64/linux_defconfig
index 8cf6abcaf..c5938d6d1 100644
--- a/board/riscv64/linux_defconfig
+++ b/board/riscv64/linux_defconfig
@@ -194,6 +194,7 @@ 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
diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig
index 7916d6c69..3a2e89380 100644
--- a/board/x86_64/linux_defconfig
+++ b/board/x86_64/linux_defconfig
@@ -221,6 +221,7 @@ 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
diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index 9620bdb3c..d52b50c23 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -24,6 +24,9 @@ All notable changes to the project are documented in this file.
   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
diff --git a/doc/qos.md b/doc/qos.md
index 89e86e10a..a06a85898 100644
--- a/doc/qos.md
+++ b/doc/qos.md
@@ -249,6 +249,30 @@ 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. + +
admin@example:/config/> edit interface e1 qos egress
+admin@example:/config/interface/e1/qos/egress/> set rate-limit rate 10000000
+admin@example:/config/interface/e1/qos/egress/> leave
+
+ +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`. A rate limited port always schedules with `tc ets`, +since `tc mqprio` can only be the root qdisc. + + ### Egress Remarking Configured under `qos egress remark`, both leaves default to `none`: @@ -355,8 +379,8 @@ and each port reports it under `qos capabilities`: `supported-trust-order` is absent when the driver has no ingress classification support. `offload` lists `classification`, `remarking` -and `transmission-selection` as the driver takes them; a stage not -listed runs in the kernel. +`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: @@ -367,6 +391,7 @@ hardware depends on the driver implementing the matching hook: | 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. /// diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c index 9d278185e..aaea17ba6 100644 --- a/src/confd/src/if-qos.c +++ b/src/confd/src/if-qos.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: BSD-3-Clause */ +#include #include #include @@ -65,6 +66,8 @@ struct qos_egress { 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 */ }; /* @@ -327,6 +330,18 @@ static int qos_parse_egress(struct lyd_node *egress, const char *ifname, struct 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; } @@ -691,13 +706,26 @@ static void gen_dcb_log(FILE *fp, const char *ifname, struct lyd_node *ingress, */ 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. mqprio can only + * be the root, so a rate limited port always schedules with ets. + */ + 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; - if (qos_mqprio(ifname) && !iface_has_quirk(ifname, "broken-mqprio")) { + if (!eg->rate && qos_mqprio(ifname) && !iface_has_quirk(ifname, "broken-mqprio")) { fprintf(fp, "tc qdisc add dev %s root mqprio num_tc %d map", ifname, eg->num_tc); for (i = 0; i < NUM_PRIO; i++) fprintf(fp, " %d", eg->map[i]); @@ -710,7 +738,7 @@ static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg) for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--) nstrict++; - fprintf(fp, "tc qdisc add dev %s root ets bands %d strict %d", ifname, eg->num_tc, 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--) @@ -831,7 +859,8 @@ int netdag_gen_qos(sr_session_ctx_t *session, struct dagger *net, struct lyd_nod */ 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", "traffic-class", NULL)) || + qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "rate-limit", NULL))) gen_egress(fp, ifname, &eg); fclose(fp); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 7be18892a..aeba520b0 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -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-09-07.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/infix-if-qos.yang b/src/confd/yang/confd/infix-if-qos.yang index 47e34c774..de37a5f4d 100644 --- a/src/confd/yang/confd/infix-if-qos.yang +++ b/src/confd/yang/confd/infix-if-qos.yang @@ -72,6 +72,14 @@ submodule infix-if-qos { * 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 { @@ -179,6 +187,9 @@ submodule infix-if-qos { 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 @@ -486,6 +497,35 @@ submodule infix-if-qos { 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/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index 451b3ed90..eeeff7d77 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -170,8 +170,8 @@ def qos_capabilities(iplink, qdiscs): 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 reports offloaded - # when the driver took it. mqprio never sets that flag, + # operations, so only those ports are asked. ets and tbf report + # offloaded when the driver took them. mqprio never sets that flag, # but it is only ever installed with hw 1, which the kernel refuses # without driver support, so its presence means the same. offload = [] @@ -187,6 +187,8 @@ def qos_capabilities(iplink, qdiscs): kind, offloaded = qdisc.get("kind"), qdisc.get("offloaded", False) if kind == "mqprio" and qdisc.get("root") or 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 diff --git a/test/case/interfaces/qos_traffic_class/test.adoc b/test/case/interfaces/qos_traffic_class/test.adoc index 35f8292c5..9ccfa4eb1 100644 --- a/test/case/interfaces/qos_traffic_class/test.adoc +++ b/test/case/interfaces/qos_traffic_class/test.adoc @@ -13,9 +13,11 @@ the priomap inverted as 802.1Q numbering requires. 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. Removing -the qos container must restore the default table, IEEE 802.1Q-2022 -Table 8-5, and the operational datastore must report whether transmission +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 @@ -29,6 +31,8 @@ image::topology.svg[QoS Traffic Classes and Transmission Selection topology, ali . 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 index 10659f77e..7fd94a289 100755 --- a/test/case/interfaces/qos_traffic_class/test.py +++ b/test/case/interfaces/qos_traffic_class/test.py @@ -11,9 +11,11 @@ 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. Removing -the qos container must restore the default table, IEEE 802.1Q-2022 -Table 8-5, and the operational datastore must report whether transmission +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 @@ -138,6 +140,32 @@ def qdisc_matches(qdisc, num_tc, prio_map, strict, quanta): }}) 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, [])) From 08403f4c973dacdc5d7d8e478d0824b835086754 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 11:20:30 +0200 Subject: [PATCH 13/21] test: docker: add iperf3, trafgen and mausezahn to the test container The QoS scheduling tests need traffic the tests do not have to write themselves: sustained UDP flows with a set rate and DSCP, counted at the receiver, and single frames with an exact VLAN tag and PCP. iperf3 comes from Alpine. Alpine has no netsniff-ng package on any branch, and mausezahn needs libcli, which only exists in edge/testing, so both are built from their release tarballs like mtools already is. netsniff-ng's configure is a bash script and its install rules need GNU install, hence bash and coreutils. Image version 2.12. Signed-off-by: Joachim Wiberg --- test/.env | 2 +- test/docker/Dockerfile | 29 ++++++++++++++++++++++++++++- test/docker/pip-requirements.txt | 4 ++++ 3 files changed, 33 insertions(+), 2 deletions(-) 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/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, From 2c49e83ece0b5cf063c664b5f1cea1b196f7b89b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 11:23:21 +0200 Subject: [PATCH 14/21] test: add QoS queue assignment test The QoS tests so far check the traffic class table as the tc arguments the daemon rendered, never the queue a frame reached. A mistake of convention, the band inversion or the priority 0 and 1 swap in Table 8-5, would render as expected and queue wrong. Send ten tagged frames at each priority with mausezahn, priority taken straight from the PCP by the ingress VLAN interface so no classifier is involved, route them out the port under test, and read the scheduler's per-class counters before and after. Run for the ieee and ieee-sr presets and a custom table. The counters are read from 'tc -s class show', which counts the CPU path on every rig. infamy/qos.py collects what the QoS tests share: the IEEE tables, capabilities, the scheduler qdisc under an optional tbf root, per-class counters in 802.1Q class numbering for both ets and mqprio, and the traffic generator wrappers. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos.yaml | 3 + .../qos_queue_assignment/Readme.adoc | 1 + .../interfaces/qos_queue_assignment/test.adoc | 35 ++++ .../interfaces/qos_queue_assignment/test.py | 142 +++++++++++++ .../qos_queue_assignment/topology.dot | 24 +++ .../qos_queue_assignment/topology.svg | 51 +++++ test/infamy/qos.py | 191 ++++++++++++++++++ 7 files changed, 447 insertions(+) create mode 120000 test/case/interfaces/qos_queue_assignment/Readme.adoc create mode 100644 test/case/interfaces/qos_queue_assignment/test.adoc create mode 100755 test/case/interfaces/qos_queue_assignment/test.py create mode 100644 test/case/interfaces/qos_queue_assignment/topology.dot create mode 100644 test/case/interfaces/qos_queue_assignment/topology.svg create mode 100644 test/infamy/qos.py diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml index 4e87ce767..748dd3d11 100644 --- a/test/case/interfaces/qos.yaml +++ b/test/case/interfaces/qos.yaml @@ -16,3 +16,6 @@ - name: QoS Bridge Classification and Remarking case: qos_bridge_classify/test.py + +- name: QoS Queue Assignment + case: qos_queue_assignment/test.py 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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/infamy/qos.py b/test/infamy/qos.py new file mode 100644 index 000000000..61fd139e0 --- /dev/null +++ b/test/infamy/qos.py @@ -0,0 +1,191 @@ +""" +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 and mqprio has classes of +its own, so everything here talks in traffic classes and hides both. +""" +import json +import re + +# 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" + +# mqprio addresses its traffic classes from this minor number upwards +MQPRIO_TC_BASE = 0xffe0 + + +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 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 or mqprio 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"] == "mqprio": + return opts.get("map", [])[:8] == list(prio_map) + 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 + if kind == "mqprio" and minor >= MQPRIO_TC_BASE: + return minor - MQPRIO_TC_BASE + 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) From f72146a67ee32186aa2ae280a161f3ba3e707701 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 16:33:08 +0200 Subject: [PATCH 15/21] test: add QoS strict priority test The QoS tests so far verify how the scheduler is configured, not how it behaves. Nothing shows that a strict priority class actually gets the port when two flows contend for it. Bridge two ports, trust DSCP on the ingress side, and slow the egress port so its queues fill: 100 Mbit/s where the PHY can negotiate down, a 10 Mbit/s rate limit otherwise. Two iperf3 UDP flows, EF and CS1, each offer 80 percent of what the port drains. In one class they split the port evenly. With EF in a strict class above CS1, EF passes without loss while CS1 takes the rest, and EF at 120 percent of the port leaves CS1 with less than 5 percent. Loss is counted as datagrams sent minus received, since the iperf3 server undercounts loss under tail drop. Priority 0 is pinned to the top class so the test's own control traffic survives the contest. Skipped on a switch fabric whose scheduler is not offloaded, where forwarded frames never meet the configured algorithm. The flow helpers, port slowing, and PMD type lookup live in infamy/qos.py for the weighted sharing and mixed selection tests that follow. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos.yaml | 3 + .../qos_strict_priority/Readme.adoc | 1 + .../interfaces/qos_strict_priority/test.adoc | 48 +++++ .../interfaces/qos_strict_priority/test.py | 142 +++++++++++++ .../qos_strict_priority/topology.dot | 24 +++ .../qos_strict_priority/topology.svg | 51 +++++ test/infamy/qos.py | 197 ++++++++++++++++++ 7 files changed, 466 insertions(+) create mode 120000 test/case/interfaces/qos_strict_priority/Readme.adoc create mode 100644 test/case/interfaces/qos_strict_priority/test.adoc create mode 100755 test/case/interfaces/qos_strict_priority/test.py create mode 100644 test/case/interfaces/qos_strict_priority/topology.dot create mode 100644 test/case/interfaces/qos_strict_priority/topology.svg diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml index 748dd3d11..5b0d8f0a6 100644 --- a/test/case/interfaces/qos.yaml +++ b/test/case/interfaces/qos.yaml @@ -19,3 +19,6 @@ - name: QoS Queue Assignment case: qos_queue_assignment/test.py + +- name: QoS Strict Priority + case: qos_strict_priority/test.py 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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/infamy/qos.py b/test/infamy/qos.py index 61fd139e0..f4b8b1802 100644 --- a/test/infamy/qos.py +++ b/test/infamy/qos.py @@ -8,6 +8,10 @@ """ 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 @@ -58,6 +62,19 @@ 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} @@ -189,3 +206,183 @@ def mausezahn(ns, iface, src_ip, dst_ip, dst_mac, count=10, delay="5msec", 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 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 + bytes 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 From 8ea11bff0c811249ef34f4e542024ff900fe47ef Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 16:37:06 +0200 Subject: [PATCH 16/21] test: add QoS weighted sharing test Nothing verifies that two enhanced transmission selection classes share the port in their configured ratio, or that the shares reach the right bands: the strict priority test only shows the top class winning. Bridge two ports as the strict priority test does, put EF and CS1 in the two lowest classes, both weighted, and offer 80 percent of the port from each so both classes always have a backlog. Equal weights must split the port evenly, which fails on its own if the shares land on the wrong bands, and 67:33 must move the split to match. Each share must land within five points of its percentage and both flows must lose frames. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos.yaml | 3 + .../qos_weighted_sharing/Readme.adoc | 1 + .../interfaces/qos_weighted_sharing/test.adoc | 46 ++++++ .../interfaces/qos_weighted_sharing/test.py | 148 ++++++++++++++++++ .../qos_weighted_sharing/topology.dot | 24 +++ .../qos_weighted_sharing/topology.svg | 51 ++++++ test/infamy/qos.py | 3 +- 7 files changed, 275 insertions(+), 1 deletion(-) create mode 120000 test/case/interfaces/qos_weighted_sharing/Readme.adoc create mode 100644 test/case/interfaces/qos_weighted_sharing/test.adoc create mode 100755 test/case/interfaces/qos_weighted_sharing/test.py create mode 100644 test/case/interfaces/qos_weighted_sharing/topology.dot create mode 100644 test/case/interfaces/qos_weighted_sharing/topology.svg diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml index 5b0d8f0a6..fd4e68f71 100644 --- a/test/case/interfaces/qos.yaml +++ b/test/case/interfaces/qos.yaml @@ -22,3 +22,6 @@ - name: QoS Strict Priority case: qos_strict_priority/test.py + +- name: QoS Weighted Sharing + case: qos_weighted_sharing/test.py 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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/infamy/qos.py b/test/infamy/qos.py index f4b8b1802..71beb6721 100644 --- a/test/infamy/qos.py +++ b/test/infamy/qos.py @@ -39,6 +39,7 @@ # mqprio addresses its traffic classes from this minor number upwards MQPRIO_TC_BASE = 0xffe0 +ETS_QUANTUM_UNIT = 1514 # one frame per percent of bandwidth, as rendered def xpath(port, path=""): @@ -342,7 +343,7 @@ def run_flows(talker, listener, dst, flows): Fills in flow.result from the listener side: bytes and packets received, lost packets and the loss in percent. Returns the total - bytes received across the flows. + datagrams received across the flows. """ servers = {} for f in flows: From 309c0bc0e1a0caced4cfa0d1505518b4fc32a978 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 16:37:26 +0200 Subject: [PATCH 17/21] test: add QoS mixed selection test The strict priority and weighted sharing tests each exercise one transmission selection algorithm. A layout with a strict class above weighted ones, the common shape for a control channel over shared bulk traffic, has no test. Bridge two ports as the other scheduling tests do, put EF in a strict class above AF21 and CS1 sharing 67:33, and run all three at once: EF at 40 percent of the port, the weighted pair at 60 percent each. EF must arrive intact, and AF21 and CS1 must both lose frames and divide what EF leaves in their configured ratio, within five points. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos.yaml | 3 + .../qos_mixed_selection/Readme.adoc | 1 + .../interfaces/qos_mixed_selection/test.adoc | 38 ++++++ .../interfaces/qos_mixed_selection/test.py | 126 ++++++++++++++++++ .../qos_mixed_selection/topology.dot | 24 ++++ .../qos_mixed_selection/topology.svg | 51 +++++++ 6 files changed, 243 insertions(+) create mode 120000 test/case/interfaces/qos_mixed_selection/Readme.adoc create mode 100644 test/case/interfaces/qos_mixed_selection/test.adoc create mode 100755 test/case/interfaces/qos_mixed_selection/test.py create mode 100644 test/case/interfaces/qos_mixed_selection/topology.dot create mode 100644 test/case/interfaces/qos_mixed_selection/topology.svg diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml index fd4e68f71..9ac6befed 100644 --- a/test/case/interfaces/qos.yaml +++ b/test/case/interfaces/qos.yaml @@ -25,3 +25,6 @@ - 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_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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + From c0a3032d8b30d34bf9b91b35ebbe5722bb7c5f2a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 17:15:15 +0200 Subject: [PATCH 18/21] linux: offload QoS transmission selection and rate limit on Marvell LinkStreet The LinkStreet patches offload classification, remarking and the class map, but the scheduler stays at the chip's power-on weighted round robin whatever the traffic-class list says, and a rate limit shapes only what the CPU sends. Two more mv88e6xxx patches take the ets and tbf qdiscs: strict classes become the port's strict queues, weighted shares become the chip's round robin weights, and the rate limit becomes the port's egress shaper. The weights are one set per chip, so ports with weighted classes must agree on their shares; a port that differs stays in software and says so in its offload list. Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 5 +- doc/qos.md | 55 +- ...-default-priority-init-on-unsupporte.patch | 2 +- ...net-dsa-Support-the-PCP-APP-selector.patch | 2 +- ...net-dsa-Support-DCB-priority-rewrite.patch | 2 +- ...6xxx-Add-DCB-priority-classification.patch | 2 +- ...a-mv88e6xxx-Add-DCB-priority-rewrite.patch | 2 +- ...e6xxx-Queue-frames-by-the-mqprio-map.patch | 2 +- ...Read-the-tag-priority-before-the-hea.patch | 4 +- ...-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch | 275 +++++++++ ...-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch | 554 ++++++++++++++++++ 11 files changed, 870 insertions(+), 35 deletions(-) create mode 100644 patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch create mode 100644 patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index d52b50c23..c4e3f7731 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -31,8 +31,9 @@ All notable changes to the project are documented in this file. 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 and remarking to the switch, so they apply - to hardware forwarded traffic as well + 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/qos.md b/doc/qos.md index a06a85898..d60cd747b 100644 --- a/doc/qos.md +++ b/doc/qos.md @@ -410,7 +410,7 @@ 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 | +| 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 | @@ -461,30 +461,35 @@ orders are accepted. Two hardware details show through: 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` applies to hardware forwarded frames as well: -each priority is queued in the first queue of its traffic class. The -class algorithms and weights are not offloaded, however. The switch -serves its eight queues by the fixed Weighted Round Robin (WRR)[^11] -weights below, whatever the `traffic-class` list says, for frames the -CPU sends as well as for forwarded ones. - -| Queue | Weight | -|------:|-------:| -| 0 | 1 | -| 1 | 2 | -| 2 | 3 | -| 3 | 6 | -| 4 | 12 | -| 5 | 17 | -| 6 | 25 | -| 7 | 33 | -/// table-caption -Marvell LinkStreet WRR weights per output queue. -/// - -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. +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. + +Without any `qos` configuration the class table is IEEE 802.1Q-2022 +Table 8-5 with every class strict, as on any other port. ## VLAN Interfaces 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 index 8b2252a18..ee419346a 100644 --- 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 @@ -1,7 +1,7 @@ From b0b26eb2286e08e25cb49e75f967105ba162f7d3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:04:21 +0200 -Subject: [PATCH 69/75] net: dsa: Skip DCB default priority init on unsupported +Subject: [PATCH 69/77] net: dsa: Skip DCB default priority init on unsupported switches Organization: Wires 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 index f4e0a7aa7..257168fe5 100644 --- 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 @@ -1,7 +1,7 @@ From 797dd2550e75613fb6eade3f1244be137a8113b4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:04:22 +0200 -Subject: [PATCH 70/75] net: dsa: Support the PCP APP selector +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 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 index daebcfcab..0fee7bf3e 100644 --- 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 @@ -1,7 +1,7 @@ From c2319721ee7343981dcd0c57fd16a651db865381 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:10:32 +0200 -Subject: [PATCH 71/75] net: dsa: Support DCB priority rewrite +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 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 index 24efba0d8..b840707bb 100644 --- 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 @@ -1,7 +1,7 @@ From 7221886d68f43d884fca5b6adf9a9b62b4c8142b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:10:33 +0200 -Subject: [PATCH 72/75] net: dsa: mv88e6xxx: Add DCB priority classification +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 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 index e5fe77df6..b356f568d 100644 --- 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 @@ -1,7 +1,7 @@ From 25811498ff5773e47d29b27b2786082653941dde Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:12:40 +0200 -Subject: [PATCH 73/75] net: dsa: mv88e6xxx: Add DCB priority rewrite +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 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 index 4746da936..d6d2b59ed 100644 --- 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 @@ -1,7 +1,7 @@ From 48aee1f644470643a38cf236ae913fe08601010e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 18:31:30 +0200 -Subject: [PATCH 74/75] net: dsa: mv88e6xxx: Queue frames by the mqprio map +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 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 index 0ca63e304..58184271f 100644 --- 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 @@ -1,7 +1,7 @@ -From 140ad7beeb39d9d7ad8e0e4982a68f501e4dee70 Mon Sep 17 00:00:00 2001 +From b8d9f22c758ac3d7a8ff56ffabe0d886f84161a1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 11 Sep 2026 15:54:16 +0200 -Subject: [PATCH 75/75] net: dsa: tag_dsa: Read the tag priority before the +Subject: [PATCH 75/77] net: dsa: tag_dsa: Read the tag priority before the header is stripped Organization: Wires 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 + From aa70f114e72fef5e8e3a860c8dc86d136f3b4641 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 17:15:16 +0200 Subject: [PATCH 19/21] confd: schedule every port with ets The renderer kept mqprio for the one fabric that offloaded its class map through it and nothing through ets. Its driver now takes ets, so the exception can go, along with the imx8mp quirk that disabled it: every port gets the same ets rendering, and the operational offload list follows the qdisc's offloaded flag alone. Signed-off-by: Joachim Wiberg --- .../etc/product/interface-quirks.json | 3 -- doc/qos.md | 3 +- src/confd/src/if-qos.c | 43 ++++--------------- .../python/yanger/ietf_interfaces/link.py | 6 +-- 4 files changed, 12 insertions(+), 43 deletions(-) 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/doc/qos.md b/doc/qos.md index d60cd747b..48ae70ee7 100644 --- a/doc/qos.md +++ b/doc/qos.md @@ -269,8 +269,7 @@ 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`. A rate limited port always schedules with `tc ets`, -since `tc mqprio` can only be the root qdisc. +`rate-limit`. ### Egress Remarking diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c index aaea17ba6..a333d89ac 100644 --- a/src/confd/src/if-qos.c +++ b/src/confd/src/if-qos.c @@ -114,11 +114,10 @@ static const struct { const char *driver; const char *orders[5]; bool pcp_encoded; /* fabric always encodes PCP from the priority */ - bool mqprio; /* offloads the class map through mqprio only */ } dcb_drivers[] = { - { "sparx5-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false, false }, - { "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false, false }, - { "mv88e6085", { "pcp", "dscp", "pcp-dscp", "dscp-pcp", NULL }, true, true }, + { "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 }, }; /* @@ -226,19 +225,6 @@ static bool qos_pcp_encoded(const char *ifname) return i >= 0 && dcb_drivers[i].pcp_encoded; } -/* - * ets carries the whole of transmission selection, and a driver that - * offloads it says so in the qdisc, so it is what every port gets. - * The one fabric that offloads the class map through mqprio, and - * nothing through ets, keeps mqprio until its driver learns ets. - */ -static bool qos_mqprio(const char *ifname) -{ - int i = qos_dcb_driver(ifname); - - return i >= 0 && dcb_drivers[i].mqprio; -} - /* Unknown drivers are not limited: without DCB the order is honoured in software. */ static bool qos_trust_supported(const char *ifname, const char *order) { @@ -698,11 +684,11 @@ static void gen_dcb_log(FILE *fp, const char *ifname, struct lyd_node *ingress, } /* - * tc mqprio takes the 802.1Q map as-is and offloads the class layout to - * the driver. tc ets is the software rendering: 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. + * 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) { @@ -713,8 +699,7 @@ static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg) /* * The rate limit is one bucket on the whole port, so it takes - * the root and the scheduler hangs below it. mqprio can only - * be the root, so a rate limited port always schedules with ets. + * 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" @@ -725,16 +710,6 @@ static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg) if (eg->num_tc < 2) return; - if (!eg->rate && qos_mqprio(ifname) && !iface_has_quirk(ifname, "broken-mqprio")) { - fprintf(fp, "tc qdisc add dev %s root mqprio num_tc %d map", ifname, eg->num_tc); - for (i = 0; i < NUM_PRIO; i++) - fprintf(fp, " %d", eg->map[i]); - fputs(" queues", fp); - for (i = 0; i < eg->num_tc; i++) - fprintf(fp, " 1@%d", i); - fputs(" hw 1 2>/dev/null ||\n", fp); - } - for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--) nstrict++; diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index eeeff7d77..19143beaa 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -171,9 +171,7 @@ def qos_capabilities(iplink, qdiscs): # 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. mqprio never sets that flag, - # but it is only ever installed with hw 1, which the kernel refuses - # without driver support, so its presence means the same. + # offloaded when the driver took them. offload = [] if trust: app = HOST.run_json(["dcb", "-j", "app", "show", "dev", ifname], {}) @@ -185,7 +183,7 @@ def qos_capabilities(iplink, qdiscs): for qdisc in qdiscs.get(ifname, []): kind, offloaded = qdisc.get("kind"), qdisc.get("offloaded", False) - if kind == "mqprio" and qdisc.get("root") or kind == "ets" and offloaded: + if kind == "ets" and offloaded: offload.append("transmission-selection") if kind == "tbf" and qdisc.get("root") and offloaded: offload.append("rate-limit") From b733f76571cb4d24f009c474a4ceb2bbeb78a965 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 17 Sep 2026 17:15:16 +0200 Subject: [PATCH 20/21] test: drop the mqprio branches from the QoS tests No port renders mqprio any more. The traffic class test expects the offload capability to follow the ets qdisc's offloaded flag, and the scheduling tests skip a switch port whose driver did not take the scheduler, since the fabric forwards past a software one. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos_factory_default/test.py | 2 -- test/case/interfaces/qos_traffic_class/test.adoc | 8 ++++---- test/case/interfaces/qos_traffic_class/test.py | 14 ++++++-------- test/infamy/qos.py | 12 +++--------- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/test/case/interfaces/qos_factory_default/test.py b/test/case/interfaces/qos_factory_default/test.py index ee978d401..2cb9c9787 100755 --- a/test/case/interfaces/qos_factory_default/test.py +++ b/test/case/interfaces/qos_factory_default/test.py @@ -84,8 +84,6 @@ def default_table(): if not qdisc: return False opts = qdisc.get("options", {}) - if qdisc["kind"] == "mqprio": - return opts.get("map", [])[:8] == TABLE_8_5[num_tc] 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]]) diff --git a/test/case/interfaces/qos_traffic_class/test.adoc b/test/case/interfaces/qos_traffic_class/test.adoc index 9ccfa4eb1..0befc650b 100644 --- a/test/case/interfaces/qos_traffic_class/test.adoc +++ b/test/case/interfaces/qos_traffic_class/test.adoc @@ -6,10 +6,10 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_traffic_class] 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. A driver with mqprio offload shows an offloaded -mqprio with the same map; otherwise the kernel runs 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. +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 diff --git a/test/case/interfaces/qos_traffic_class/test.py b/test/case/interfaces/qos_traffic_class/test.py index 7fd94a289..8622297f0 100755 --- a/test/case/interfaces/qos_traffic_class/test.py +++ b/test/case/interfaces/qos_traffic_class/test.py @@ -4,10 +4,10 @@ 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. A driver with mqprio offload shows an offloaded -mqprio with the same map; otherwise the kernel runs 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. +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 @@ -69,8 +69,6 @@ def qdisc_matches(qdisc, num_tc, prio_map, strict, quanta): if not qdisc: return False opts = qdisc.get("options", {}) - if qdisc["kind"] == "mqprio": - return opts.get("map", [])[:8] == prio_map if qdisc["kind"] == "ets": return (opts.get("bands") == num_tc and opts.get("strict") == strict and opts.get("quanta", []) == quanta @@ -122,9 +120,9 @@ def qdisc_matches(qdisc, num_tc, prio_map, strict, quanta): print(json.dumps(root_qdisc(tgtssh, port))) with test.step("Verify the offload capability matches the qdisc"): - kind = root_qdisc(tgtssh, port)["kind"] + taken = root_qdisc(tgtssh, port).get("offloaded", False) until(lambda: ("transmission-selection" in capabilities(target, port).get("offload", [])) - == (kind == "mqprio")) + == taken) with test.step("Switch to the ieee-sr preset and verify SR classes on top"): target.delete_xpath(qos_xpath(port, "/egress")) diff --git a/test/infamy/qos.py b/test/infamy/qos.py index 71beb6721..fed5c0ae2 100644 --- a/test/infamy/qos.py +++ b/test/infamy/qos.py @@ -3,8 +3,8 @@ 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 and mqprio has classes of -its own, so everything here talks in traffic classes and hides both. +tc ets qdisc numbers its bands the other way, so everything here talks +in traffic classes and hides that. """ import json import re @@ -37,8 +37,6 @@ STRICT = "ieee802-dot1q-types:strict-priority" ETS = "ieee802-dot1q-types:enhanced-transmission-selection" -# mqprio addresses its traffic classes from this minor number upwards -MQPRIO_TC_BASE = 0xffe0 ETS_QUANTUM_UNIT = 1514 # one frame per percent of bandwidth, as rendered @@ -101,7 +99,7 @@ def root_qdisc(ssh, port): def scheduler(ssh, port): - """The ets or mqprio qdisc: the root, or the child of a tbf root""" + """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): @@ -120,8 +118,6 @@ def scheduler_matches(qdisc, num_tc, prio_map, strict=None, quanta=None): if not qdisc: return False opts = qdisc.get("options", {}) - if qdisc["kind"] == "mqprio": - return opts.get("map", [])[:8] == list(prio_map) if qdisc["kind"] == "ets": if opts.get("bands") != num_tc: return False @@ -149,8 +145,6 @@ 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 - if kind == "mqprio" and minor >= MQPRIO_TC_BASE: - return minor - MQPRIO_TC_BASE return None if out.startswith("["): From df12cdc8b0558cbcf05aad7c785fb52418154ed1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 18 Sep 2026 12:18:49 +0200 Subject: [PATCH 21/21] test: add QoS rate limit test The traffic class test checks that a rate limit renders a tbf root, not that the port drains at the configured rate, and the scheduling tests lean on that limit to fill the port without proving it holds. Bridge two ports, limit the egress port to 10 Mbit/s and offer three times that from one talker: what arrives must be the limit within a fifth, once marked EF in a strict class, once marked CS1 in the lowest class, and once with both talkers at one and a half times the limit. A limit that only bites on some queues is a scheduler fault dressed up as one. Skipped on a switch fabric whose driver does not offload the limit, where forwarded frames never meet the bucket. Signed-off-by: Joachim Wiberg --- test/case/interfaces/qos.yaml | 3 + .../interfaces/qos_rate_limit/Readme.adoc | 1 + test/case/interfaces/qos_rate_limit/test.adoc | 33 ++++++ test/case/interfaces/qos_rate_limit/test.py | 104 ++++++++++++++++++ .../interfaces/qos_rate_limit/topology.dot | 24 ++++ .../interfaces/qos_rate_limit/topology.svg | 51 +++++++++ test/infamy/qos.py | 4 + 7 files changed, 220 insertions(+) create mode 120000 test/case/interfaces/qos_rate_limit/Readme.adoc create mode 100644 test/case/interfaces/qos_rate_limit/test.adoc create mode 100755 test/case/interfaces/qos_rate_limit/test.py create mode 100644 test/case/interfaces/qos_rate_limit/topology.dot create mode 100644 test/case/interfaces/qos_rate_limit/topology.svg diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml index 9ac6befed..e048105b8 100644 --- a/test/case/interfaces/qos.yaml +++ b/test/case/interfaces/qos.yaml @@ -20,6 +20,9 @@ - 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 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 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/infamy/qos.py b/test/infamy/qos.py index fed5c0ae2..aedce2f1c 100644 --- a/test/infamy/qos.py +++ b/test/infamy/qos.py @@ -328,6 +328,10 @@ def __init__(self, name, port, tos, rate_bps, seconds=4, size=1000): 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