Skip to content

Commit 278fba2

Browse files
authored
Merge pull request #5305 from natali-rs1985/T8996
qos: T8996: Implement set-dscp packet remarking for shaper policy
2 parents 4d33647 + a726455 commit 278fba2

2 files changed

Lines changed: 156 additions & 9 deletions

File tree

python/vyos/qos/base.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ def update(self, config, direction, priority=None):
241241
for cls, cls_config in config['class'].items():
242242
self._build_base_qdisc(cls_config, int(cls))
243243

244+
245+
# Get DSCP value for packet remarking via tc pedit action
246+
set_dscp = dict_search('set_dscp', cls_config)
247+
dscp_value = None
248+
if set_dscp:
249+
dscp_value = str(self._get_dsfield(set_dscp))
250+
244251
# every match criteria has it's tc instance
245252
filter_cmd_base = ['tc', 'filter', 'add', 'dev', self._interface,
246253
'parent', f'{self._parent:x}:']
@@ -263,8 +270,10 @@ def update(self, config, direction, priority=None):
263270
has_filter = True
264271
break
265272

266-
tmp = dict_search(f'ether.protocol', match_config) or 'all'
267-
filter_cmd += ['protocol', str(tmp)]
273+
filter_protocol = (
274+
dict_search(f'ether.protocol', match_config) or 'all'
275+
)
276+
filter_cmd += ['protocol', filter_protocol]
268277

269278
if self.qostype in ['shaper', 'shaper_hfsc'] and 'prio' not in filter_cmd:
270279
filter_cmd += ['prio', str(index)]
@@ -361,9 +370,29 @@ def update(self, config, direction, priority=None):
361370
elif af == 'ipv6':
362371
filter_cmd += ['match', 'u8', str(mask), str(mask), 'at', '53']
363372

373+
# Build pedit action to rewrite DSCP on matched packets.
374+
# retain 0xfc preserves ECN bits (bottom 2 bits of TOS/Traffic Class).
375+
# Non-IP match types skip pedit to avoid corrupting
376+
# non-IP packets, unless ether protocol is ip or ipv6.
377+
dscp_action = []
378+
if dscp_value is not None:
379+
proto = str(filter_protocol).lower()
380+
is_ipv4 = 'ip' in match_config or proto in ('ip', '0x0800', '2048')
381+
is_ipv6 = 'ipv6' in match_config or proto in ('ipv6', '0x86dd', '34525')
382+
if is_ipv4:
383+
dscp_action = ['action', 'pedit', 'ex', 'munge', 'ip',
384+
'dsfield', 'set', dscp_value, 'retain', '0xfc',
385+
'pipe', 'action', 'csum', 'ip4h']
386+
elif is_ipv6:
387+
dscp_action = ['action', 'pedit', 'ex', 'munge',
388+
'ip6', 'traffic_class', 'set', dscp_value, 'retain', '0xfc']
389+
364390
if index != max_index or not has_action_policy:
365391
# avoid duplicate last match rule
366392
cls = int(cls)
393+
# add pedit before flowid for filters without police
394+
if dscp_action:
395+
filter_cmd += dscp_action
367396
filter_cmd += ['flowid', f'{self._parent:x}:{cls:x}']
368397
self._cmdl(filter_cmd)
369398

@@ -373,6 +402,9 @@ def update(self, config, direction, priority=None):
373402
if has_action_policy and has_filter:
374403
# For "vif" "basic match" is used instead of "action police" T5961
375404
if not match_vlan:
405+
# chain pedit before police with pipe
406+
if dscp_action:
407+
filter_cmd += dscp_action + ['pipe']
376408
filter_cmd += ['action', 'police']
377409

378410
if 'exceed' in cls_config:
@@ -393,6 +425,9 @@ def update(self, config, direction, priority=None):
393425
if 'mtu' in cls_config:
394426
mtu = cls_config['mtu']
395427
filter_cmd += ['mtu', str(mtu)]
428+
elif dscp_action:
429+
# vlan match skips police (T5961) but still needs pedit
430+
filter_cmd += dscp_action
396431

397432
cls = int(cls)
398433
filter_cmd += ['flowid', f'{self._parent:x}:{cls:x}']
@@ -430,6 +465,32 @@ def update(self, config, direction, priority=None):
430465
default_cls_id = int(class_id_max) +1
431466
self._build_base_qdisc(config['default'], default_cls_id)
432467

468+
# Default class has no match filters, so catch-all filters
469+
# are needed to attach the pedit action for DSCP remarking.
470+
# Separate filters per protocol to avoid corrupting non-IP packets (e.g. ARP).
471+
# IPv4 uses u32 catch-all, IPv6 uses basic classifier (u32 doesn't support protocol ipv6).
472+
# prio 255/256 ensures class filters match first.
473+
set_dscp = dict_search('set_dscp', config['default'])
474+
if set_dscp and self.qostype == 'shaper':
475+
dscp_value = str(self._get_dsfield(set_dscp))
476+
filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface,
477+
'parent', f'{self._parent:x}:']
478+
filter_cmd += ['prio', '255', 'protocol', 'ip', 'u32',
479+
'match', 'u32', '0', '0']
480+
filter_cmd += ['action', 'pedit', 'ex', 'munge',
481+
'ip', 'dsfield', 'set', dscp_value, 'retain', '0xfc',
482+
'pipe', 'action', 'csum', 'ip4h']
483+
filter_cmd += ['flowid', f'{self._parent:x}:{default_cls_id:x}']
484+
self._cmdl(filter_cmd)
485+
486+
filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface,
487+
'parent', f'{self._parent:x}:']
488+
filter_cmd += ['prio', '256', 'protocol', 'ipv6', 'basic']
489+
filter_cmd += ['action', 'pedit', 'ex', 'munge', 'ip6',
490+
'traffic_class', 'set', dscp_value, 'retain', '0xfc']
491+
filter_cmd += ['flowid', f'{self._parent:x}:{default_cls_id:x}']
492+
self._cmdl(filter_cmd)
493+
433494
if self.qostype == 'limiter':
434495
if 'default' in config:
435496
filter_cmd = ['tc', 'filter', 'replace', 'dev', self._interface,

smoketest/scripts/cli/test_qos.py

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -614,15 +614,23 @@ def test_11_shaper(self):
614614

615615
for interface in self._interfaces:
616616
shaper_name = f'qos-shaper-{interface}'
617+
shaper_path = base_path + ['policy', 'shaper', shaper_name]
618+
class_path = shaper_path + ['class', '23']
617619

618620
self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
619-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit'])
620-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit'])
621-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'ceiling', f'{default_ceil}mbit'])
622-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'fair-queue'])
623-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'bandwidth', f'{class_bandwidth}mbit'])
624-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'ceiling', f'{class_ceil}mbit'])
625-
self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ip', 'destination', 'address', dst_address])
621+
self.cli_set(shaper_path + ['bandwidth', f'{bandwidth}mbit'])
622+
self.cli_set(
623+
shaper_path + ['default', 'bandwidth', f'{default_bandwidth}mbit']
624+
)
625+
self.cli_set(shaper_path + ['default', 'ceiling', f'{default_ceil}mbit'])
626+
self.cli_set(shaper_path + ['default', 'queue-type', 'fair-queue'])
627+
self.cli_set(shaper_path + ['default', 'set-dscp', 'AF11'])
628+
self.cli_set(class_path + ['bandwidth', f'{class_bandwidth}mbit'])
629+
self.cli_set(class_path + ['ceiling', f'{class_ceil}mbit'])
630+
self.cli_set(
631+
class_path
632+
+ ['match', '10', 'ip', 'destination', 'address', dst_address]
633+
)
626634

627635
bandwidth += 1
628636
default_bandwidth += 1
@@ -651,6 +659,16 @@ def test_11_shaper(self):
651659
for config_entry in config_entries:
652660
self.assertIn(config_entry, output)
653661

662+
# set-dscp on default class: catch-all filters with pedit
663+
# AF11 = DSCP 10 << 2 = 0x28
664+
filter_output = get_tc_filter_details(interface)
665+
self.assertIn('pedit', filter_output)
666+
self.assertIn('protocol ip pref 255', filter_output)
667+
self.assertIn('at ipv4+0: val 00280000', filter_output)
668+
self.assertIn('csum (iph)', filter_output)
669+
self.assertIn('protocol ipv6 pref 256', filter_output)
670+
self.assertIn('at ipv6+0: val 02800000', filter_output)
671+
654672
bandwidth += 1
655673
default_bandwidth += 1
656674
default_ceil += 1
@@ -1325,5 +1343,73 @@ def test_24_policy_shaper_match_ether(self):
13251343
get_tc_filter_details(interface))
13261344

13271345

1346+
def test_25_shaper_set_dscp(self):
1347+
interface = self._interfaces[0]
1348+
shaper_name = f'qos-shaper-{interface}'
1349+
shaper_path = base_path + ['policy', 'shaper', shaper_name]
1350+
class_path = shaper_path + ['class', '10']
1351+
1352+
self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
1353+
self.cli_set(shaper_path + ['bandwidth', '100mbit'])
1354+
self.cli_set(shaper_path + ['default', 'bandwidth', '10mbit'])
1355+
self.cli_set(class_path + ['bandwidth', '50mbit'])
1356+
self.cli_set(class_path + ['set-dscp', 'CS4'])
1357+
1358+
# IPv4 match: pedit should target ipv4+0
1359+
self.cli_set(
1360+
class_path + ['match', 'RULE', 'ip', 'source', 'address', '172.17.1.2/32']
1361+
)
1362+
self.cli_commit()
1363+
1364+
# CS4 = DSCP 32 << 2 = 0x80
1365+
filter_output = get_tc_filter_details(interface)
1366+
self.assertIn('action order 1: pedit', filter_output)
1367+
self.assertIn('at ipv4+0: val 00800000', filter_output)
1368+
self.assertIn('csum (iph)', filter_output)
1369+
self.assertNotIn('at ipv6+0', filter_output)
1370+
1371+
# IPv6 match: pedit should target ipv6+0
1372+
self.cli_delete(class_path + ['match', 'RULE'])
1373+
self.cli_set(
1374+
class_path + ['match', 'RULE', 'ipv6', 'source', 'address', '2001:db8::/32']
1375+
)
1376+
self.cli_set(class_path + ['set-dscp', 'AF21'])
1377+
self.cli_commit()
1378+
1379+
# AF21 = DSCP 18 << 2 = 0x48
1380+
filter_output = get_tc_filter_details(interface)
1381+
self.assertIn('action order 1: pedit', filter_output)
1382+
self.assertIn('at ipv6+0: val 04800000', filter_output)
1383+
self.assertNotIn('at ipv4+0', filter_output)
1384+
1385+
# Ether match with protocol all: pedit is skipped to avoid
1386+
# corrupting non-IP packets
1387+
self.cli_delete(class_path + ['match', 'RULE'])
1388+
self.cli_set(class_path + ['match', 'RULE', 'ether', 'protocol', 'all'])
1389+
self.cli_set(class_path + ['set-dscp', '46'])
1390+
self.cli_commit()
1391+
1392+
filter_output = get_tc_filter_details(interface)
1393+
self.assertNotIn('pedit', filter_output)
1394+
1395+
# Ether match with protocol ip: pedit targets ipv4+0 only
1396+
self.cli_set(class_path + ['match', 'RULE', 'ether', 'protocol', 'ip'])
1397+
self.cli_commit()
1398+
1399+
# numeric 46 << 2 = 0xb8
1400+
filter_output = get_tc_filter_details(interface)
1401+
self.assertIn('action order 1: pedit', filter_output)
1402+
self.assertIn('at ipv4+0: val 00b80000', filter_output)
1403+
self.assertIn('csum (iph)', filter_output)
1404+
self.assertNotIn('at ipv6+0', filter_output)
1405+
1406+
# Removing set-dscp: no pedit should remain
1407+
self.cli_delete(class_path + ['set-dscp'])
1408+
self.cli_commit()
1409+
1410+
filter_output = get_tc_filter_details(interface)
1411+
self.assertNotIn('pedit', filter_output)
1412+
1413+
13281414
if __name__ == '__main__':
13291415
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())

0 commit comments

Comments
 (0)