diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-06-30 14:46:23 +0300 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2026-07-28 14:58:05 +0300 |
| commit | a726455aadcafd27e489314a3a1630919296cbee (patch) | |
| tree | eda96310069e860468025d828b998f5be97392a9 /python | |
| parent | b81e435210f499b225b5d27fd16c370c0095da3a (diff) | |
| download | vyos-1x-a726455aadcafd27e489314a3a1630919296cbee.tar.gz vyos-1x-a726455aadcafd27e489314a3a1630919296cbee.zip | |
qos: T8996: Implement set-dscp packet remarking for shaper policy
The set-dscp option was defined in XML but never implemented in
Python — no tc commands were generated to rewrite DSCP on egress.
Add tc pedit actions to shaper filter commands. The pedit target
is chosen per-match: IPv4 uses "ip dsfield", IPv6 uses
"ip6 traffic_class". Ether matches with protocol ip or ipv6 also
get the corresponding pedit; other non-IP match types skip pedit
to avoid corrupting packets like ARP. The retain 0xfc mask
preserves ECN bits. For IPv4, a csum ip4h action recalculates
the header checksum after pedit. For the default class, separate
catch-all filters (prio 255/256) are added per protocol.
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/qos/base.py | 65 |
1 files changed, 63 insertions, 2 deletions
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 65991704d..d61ad78be 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -241,6 +241,13 @@ class QoSBase: for cls, cls_config in config['class'].items(): self._build_base_qdisc(cls_config, int(cls)) + + # Get DSCP value for packet remarking via tc pedit action + set_dscp = dict_search('set_dscp', cls_config) + dscp_value = None + if set_dscp: + dscp_value = str(self._get_dsfield(set_dscp)) + # every match criteria has it's tc instance filter_cmd_base = ['tc', 'filter', 'add', 'dev', self._interface, 'parent', f'{self._parent:x}:'] @@ -263,8 +270,10 @@ class QoSBase: has_filter = True break - tmp = dict_search(f'ether.protocol', match_config) or 'all' - filter_cmd += ['protocol', str(tmp)] + filter_protocol = ( + dict_search(f'ether.protocol', match_config) or 'all' + ) + filter_cmd += ['protocol', filter_protocol] if self.qostype in ['shaper', 'shaper_hfsc'] and 'prio' not in filter_cmd: filter_cmd += ['prio', str(index)] @@ -361,9 +370,29 @@ class QoSBase: elif af == 'ipv6': filter_cmd += ['match', 'u8', str(mask), str(mask), 'at', '53'] + # Build pedit action to rewrite DSCP on matched packets. + # retain 0xfc preserves ECN bits (bottom 2 bits of TOS/Traffic Class). + # Non-IP match types skip pedit to avoid corrupting + # non-IP packets, unless ether protocol is ip or ipv6. + dscp_action = [] + if dscp_value is not None: + proto = str(filter_protocol).lower() + is_ipv4 = 'ip' in match_config or proto in ('ip', '0x0800', '2048') + is_ipv6 = 'ipv6' in match_config or proto in ('ipv6', '0x86dd', '34525') + if is_ipv4: + dscp_action = ['action', 'pedit', 'ex', 'munge', 'ip', + 'dsfield', 'set', dscp_value, 'retain', '0xfc', + 'pipe', 'action', 'csum', 'ip4h'] + elif is_ipv6: + dscp_action = ['action', 'pedit', 'ex', 'munge', + 'ip6', 'traffic_class', 'set', dscp_value, 'retain', '0xfc'] + if index != max_index or not has_action_policy: # avoid duplicate last match rule cls = int(cls) + # add pedit before flowid for filters without police + if dscp_action: + filter_cmd += dscp_action filter_cmd += ['flowid', f'{self._parent:x}:{cls:x}'] self._cmdl(filter_cmd) @@ -373,6 +402,9 @@ class QoSBase: if has_action_policy and has_filter: # For "vif" "basic match" is used instead of "action police" T5961 if not match_vlan: + # chain pedit before police with pipe + if dscp_action: + filter_cmd += dscp_action + ['pipe'] filter_cmd += ['action', 'police'] if 'exceed' in cls_config: @@ -393,6 +425,9 @@ class QoSBase: if 'mtu' in cls_config: mtu = cls_config['mtu'] filter_cmd += ['mtu', str(mtu)] + elif dscp_action: + # vlan match skips police (T5961) but still needs pedit + filter_cmd += dscp_action cls = int(cls) filter_cmd += ['flowid', f'{self._parent:x}:{cls:x}'] @@ -430,6 +465,32 @@ class QoSBase: default_cls_id = int(class_id_max) +1 self._build_base_qdisc(config['default'], default_cls_id) + # Default class has no match filters, so catch-all filters + # are needed to attach the pedit action for DSCP remarking. + # Separate filters per protocol to avoid corrupting non-IP packets (e.g. ARP). + # IPv4 uses u32 catch-all, IPv6 uses basic classifier (u32 doesn't support protocol ipv6). + # prio 255/256 ensures class filters match first. + set_dscp = dict_search('set_dscp', config['default']) + if set_dscp and self.qostype == 'shaper': + dscp_value = str(self._get_dsfield(set_dscp)) + filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface, + 'parent', f'{self._parent:x}:'] + filter_cmd += ['prio', '255', 'protocol', 'ip', 'u32', + 'match', 'u32', '0', '0'] + filter_cmd += ['action', 'pedit', 'ex', 'munge', + 'ip', 'dsfield', 'set', dscp_value, 'retain', '0xfc', + 'pipe', 'action', 'csum', 'ip4h'] + filter_cmd += ['flowid', f'{self._parent:x}:{default_cls_id:x}'] + self._cmdl(filter_cmd) + + filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface, + 'parent', f'{self._parent:x}:'] + filter_cmd += ['prio', '256', 'protocol', 'ipv6', 'basic'] + filter_cmd += ['action', 'pedit', 'ex', 'munge', 'ip6', + 'traffic_class', 'set', dscp_value, 'retain', '0xfc'] + filter_cmd += ['flowid', f'{self._parent:x}:{default_cls_id:x}'] + self._cmdl(filter_cmd) + if self.qostype == 'limiter': if 'default' in config: filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface, |
