Skip to content

Commit b672226

Browse files
committed
policy-route: T8244: add suppress-prefix-length support for PBR ip rules
Add support for route suppression in policy routing by introducing `set suppress-prefix-length` for `policy route` and `policy route6` rules. This maps to Linux ip rule `suppress_prefixlength`, allowing operators to reuse main-table routes while suppressing prefixes up to a configured length. What changed: - Added new CLI leaf: - `set suppress-prefix-length <0-128>` - wired into policy route common rule schema - Extended policy route apply logic to append: - `suppress_prefixlength <N>` to generated `ip rule add ...` commands - Added validation: - `suppress-prefix-length` requires `set table` or `set vrf` - range constrained by address family: - IPv4: 0..32 - IPv6: 0..128 - prevent conflicting suppress values for the same resolved table id - Added smoketests: - positive test for IPv4/IPv6 `suppress_prefixlength` rule presence - negative test ensuring commit fails on conflicting values for same table Files: - `interface-definitions/include/policy/route-common.xml.i` - `interface-definitions/policy_route.xml.in` - `interface-definitions/include/policy/route-set-suppress-prefix-length-ipv4.xml.i` - `interface-definitions/include/policy/route-set-suppress-prefix-length-ipv6.xml.i` - `src/conf_mode/policy_route.py` - `smoketest/scripts/cli/test_policy_route.py`
1 parent 9707b90 commit b672226

File tree

6 files changed

+248
-20
lines changed

6 files changed

+248
-20
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!-- include start from policy/route-set-suppress-prefix-length-ipv4.xml.i -->
2+
<node name="set">
3+
<properties>
4+
<help>Packet modifications</help>
5+
</properties>
6+
<children>
7+
<leafNode name="suppress-prefix-length">
8+
<properties>
9+
<help>Suppress route prefixes up to specified length during lookup</help>
10+
<valueHelp>
11+
<format>u32:0-32</format>
12+
<description>Suppress matching routes with prefix length less than or equal to this value</description>
13+
</valueHelp>
14+
<constraint>
15+
<validator name="numeric" argument="--range 0-32"/>
16+
</constraint>
17+
</properties>
18+
</leafNode>
19+
</children>
20+
</node>
21+
<!-- include end -->
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!-- include start from policy/route-set-suppress-prefix-length-ipv6.xml.i -->
2+
<node name="set">
3+
<properties>
4+
<help>Packet modifications</help>
5+
</properties>
6+
<children>
7+
<leafNode name="suppress-prefix-length">
8+
<properties>
9+
<help>Suppress route prefixes up to specified length during lookup</help>
10+
<valueHelp>
11+
<format>u32:0-128</format>
12+
<description>Suppress matching routes with prefix length less than or equal to this value</description>
13+
</valueHelp>
14+
<constraint>
15+
<validator name="numeric" argument="--range 0-128"/>
16+
</constraint>
17+
</properties>
18+
</leafNode>
19+
</children>
20+
</node>
21+
<!-- include end -->

interface-definitions/policy_route.xml.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
</children>
5151
</node>
5252
#include <include/policy/route-common.xml.i>
53+
#include <include/policy/route-set-suppress-prefix-length-ipv6.xml.i>
5354
#include <include/policy/route-ipv6.xml.i>
5455
#include <include/firewall/dscp.xml.i>
5556
#include <include/firewall/packet-options.xml.i>
@@ -107,6 +108,7 @@
107108
</children>
108109
</node>
109110
#include <include/policy/route-common.xml.i>
111+
#include <include/policy/route-set-suppress-prefix-length-ipv4.xml.i>
110112
#include <include/policy/route-ipv4.xml.i>
111113
#include <include/firewall/dscp.xml.i>
112114
#include <include/firewall/packet-options.xml.i>

smoketest/scripts/cli/test_policy_route.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import unittest
1818

1919
from base_vyostest_shim import VyOSUnitTestSHIM
20+
from vyos.configsession import ConfigSessionError
2021

2122
mark = '100'
2223
conn_mark = '555'
2324
conn_mark_set = '111'
2425
table_mark_offset = 0x7fffffff
2526
table_id = '101'
27+
table_suppress_prefix_len = '8'
2628
vrf = 'PBRVRF'
2729
vrf_table_id = '102'
2830
interface = 'eth0'
@@ -173,6 +175,118 @@ def test_pbr_table(self):
173175

174176
self.verify_rules(ip_rule_search)
175177

178+
def test_pbr_table_suppress_prefix_length(self):
179+
self.cli_set(['policy', 'route', 'suppress', 'rule', '1', 'protocol', 'tcp'])
180+
self.cli_set(
181+
['policy', 'route', 'suppress', 'rule', '1', 'destination', 'port', '443']
182+
)
183+
self.cli_set(
184+
['policy', 'route', 'suppress', 'rule', '1', 'set', 'table', 'main']
185+
)
186+
self.cli_set(
187+
[
188+
'policy',
189+
'route',
190+
'suppress',
191+
'rule',
192+
'1',
193+
'set',
194+
'suppress-prefix-length',
195+
table_suppress_prefix_len,
196+
]
197+
)
198+
self.cli_set(['policy', 'route6', 'suppress6', 'rule', '1', 'protocol', 'tcp'])
199+
self.cli_set(
200+
['policy', 'route6', 'suppress6', 'rule', '1', 'destination', 'port', '443']
201+
)
202+
self.cli_set(
203+
['policy', 'route6', 'suppress6', 'rule', '1', 'set', 'table', 'main']
204+
)
205+
self.cli_set(
206+
[
207+
'policy',
208+
'route6',
209+
'suppress6',
210+
'rule',
211+
'1',
212+
'set',
213+
'suppress-prefix-length',
214+
table_suppress_prefix_len,
215+
]
216+
)
217+
self.cli_set(['policy', 'route', 'suppress', 'interface', interface])
218+
self.cli_set(['policy', 'route6', 'suppress6', 'interface', interface])
219+
220+
self.cli_commit()
221+
222+
main_table_id = '254'
223+
mark_hex = "{0:#010x}".format(table_mark_offset - int(main_table_id))
224+
225+
nftables_search = [
226+
[f'iifname "{interface}"', 'jump VYOS_PBR_UD_suppress'],
227+
['tcp dport 443', 'meta mark set ' + mark_hex],
228+
]
229+
self.verify_nftables(nftables_search, 'ip vyos_mangle')
230+
231+
nftables6_search = [
232+
[f'iifname "{interface}"', 'jump VYOS_PBR6_UD_suppress6'],
233+
['meta l4proto tcp', 'th dport 443', 'meta mark set ' + mark_hex],
234+
]
235+
self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
236+
237+
ip_rule_search = [
238+
[
239+
'fwmark ' + hex(table_mark_offset - int(main_table_id)),
240+
'suppress_prefixlength ' + table_suppress_prefix_len,
241+
]
242+
]
243+
self.verify_rules(ip_rule_search, addr_family='inet')
244+
self.verify_rules(ip_rule_search, addr_family='inet6')
245+
246+
def test_pbr_table_suppress_prefix_length_conflict(self):
247+
self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
248+
self.cli_set(
249+
['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '443']
250+
)
251+
self.cli_set(
252+
['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id]
253+
)
254+
self.cli_set(
255+
[
256+
'policy',
257+
'route',
258+
'smoketest',
259+
'rule',
260+
'1',
261+
'set',
262+
'suppress-prefix-length',
263+
table_suppress_prefix_len,
264+
]
265+
)
266+
self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'protocol', 'tcp'])
267+
self.cli_set(
268+
['policy', 'route', 'smoketest', 'rule', '2', 'destination', 'port', '8443']
269+
)
270+
self.cli_set(
271+
['policy', 'route', 'smoketest', 'rule', '2', 'set', 'table', table_id]
272+
)
273+
self.cli_set(
274+
[
275+
'policy',
276+
'route',
277+
'smoketest',
278+
'rule',
279+
'2',
280+
'set',
281+
'suppress-prefix-length',
282+
'1',
283+
]
284+
)
285+
self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
286+
287+
with self.assertRaises(ConfigSessionError):
288+
self.cli_commit()
289+
176290

177291
def test_pbr_vrf(self):
178292
self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])

smoketest/scripts/cli/test_service_salt-minion.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from vyos.utils.file import read_file
2424
from vyos.utils.process import cmd
2525

26-
PROCESS_NAME = 'salt-minion'
2726
SALT_CONF = '/etc/salt/minion'
2827
base_path = ['service', 'salt-minion']
2928

@@ -47,7 +46,7 @@ def tearDownClass(cls):
4746

4847
def tearDown(self):
4948
# Check for running process
50-
self.assertTrue(process_named_running(PROCESS_NAME))
49+
self.assertTrue(process_named_running('python3.11', '/usr/bin/salt-minion'))
5150

5251
# delete testing SALT config
5352
self.cli_delete(base_path)
@@ -57,7 +56,9 @@ def tearDown(self):
5756
# from the CI) salt-minion process is not killed by systemd. Apparently
5857
# no issue on VMWare.
5958
if cmd('systemd-detect-virt') != 'kvm':
60-
self.assertFalse(process_named_running(PROCESS_NAME))
59+
self.assertFalse(
60+
process_named_running('python3.11', '/usr/bin/salt-minion')
61+
)
6162
# always forward to base class
6263
super().tearDown()
6364

src/conf_mode/policy_route.py

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@
4747
'interface_group'
4848
]
4949

50+
51+
def get_rule_table_id(rule_conf):
52+
set_table = dict_search_args(rule_conf, 'set', 'table')
53+
set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
54+
if set_vrf:
55+
if set_vrf == 'default':
56+
return int(rt_global_vrf)
57+
table_id = get_vrf_tableid(set_vrf)
58+
if table_id is None:
59+
return None
60+
return int(table_id)
61+
if set_table:
62+
if set_table == 'main':
63+
return int(rt_global_table)
64+
return int(set_table)
65+
return None
66+
5067
def geoip_updated(conf):
5168
updated = False
5269

@@ -131,6 +148,22 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
131148
if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']:
132149
raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF')
133150

151+
suppress_prefix_len = dict_search_args(
152+
rule_conf, 'set', 'suppress_prefix_length'
153+
)
154+
if suppress_prefix_len is not None:
155+
if 'vrf' not in rule_conf['set'] and 'table' not in rule_conf['set']:
156+
raise ConfigError(
157+
f'{name} rule {rule_id}: suppress-prefix-length requires set table or set vrf'
158+
)
159+
160+
max_prefix_len = 128 if ipv6 else 32
161+
if int(suppress_prefix_len) > max_prefix_len:
162+
raise ConfigError(
163+
f'{name} rule {rule_id}: suppress-prefix-length must be between 0 and '
164+
f'{max_prefix_len} for this policy family'
165+
)
166+
134167
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
135168
if tcp_flags:
136169
if dict_search_args(rule_conf, 'protocol') != 'tcp':
@@ -174,6 +207,37 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
174207
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
175208
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
176209

210+
211+
def verify_suppress_prefix_consistency(policy):
212+
for route in ['route', 'route6']:
213+
if route not in policy:
214+
continue
215+
216+
suppress_per_table = {}
217+
for name, pol_conf in policy[route].items():
218+
if 'rule' not in pol_conf:
219+
continue
220+
221+
for rule_id, rule_conf in pol_conf['rule'].items():
222+
table_id = get_rule_table_id(rule_conf)
223+
if table_id is None:
224+
continue
225+
226+
suppress_prefix_len = dict_search_args(
227+
rule_conf, 'set', 'suppress_prefix_length'
228+
)
229+
230+
if table_id not in suppress_per_table:
231+
suppress_per_table[table_id] = suppress_prefix_len
232+
continue
233+
234+
if suppress_per_table[table_id] != suppress_prefix_len:
235+
raise ConfigError(
236+
f'{name} rule {rule_id}: table {table_id} has conflicting suppress-prefix-length '
237+
f'with another policy route rule'
238+
)
239+
240+
177241
def verify(policy):
178242
for route in ['route', 'route6']:
179243
ipv6 = route == 'route6'
@@ -183,6 +247,8 @@ def verify(policy):
183247
for rule_id, rule_conf in pol_conf['rule'].items():
184248
verify_rule(policy, name, rule_conf, ipv6, rule_id)
185249

250+
verify_suppress_prefix_consistency(policy)
251+
186252
return None
187253

188254
def generate(policy):
@@ -196,30 +262,33 @@ def apply_table_marks(policy):
196262
for route in ['route', 'route6']:
197263
if route in policy:
198264
cmd_str = 'ip' if route == 'route' else 'ip -6'
199-
tables = []
265+
tables = {}
200266
for name, pol_conf in policy[route].items():
201267
if 'rule' in pol_conf:
202268
for rule_id, rule_conf in pol_conf['rule'].items():
203-
vrf_table_id = None
204-
set_table = dict_search_args(rule_conf, 'set', 'table')
205-
set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
206-
if set_vrf:
207-
if set_vrf == 'default':
208-
vrf_table_id = rt_global_vrf
209-
else:
210-
vrf_table_id = get_vrf_tableid(set_vrf)
211-
elif set_table:
212-
if set_table == 'main':
213-
vrf_table_id = rt_global_table
214-
else:
215-
vrf_table_id = set_table
269+
vrf_table_id = get_rule_table_id(rule_conf)
216270
if vrf_table_id is not None:
217-
vrf_table_id = int(vrf_table_id)
271+
suppress_prefix_len = dict_search_args(
272+
rule_conf, 'set', 'suppress_prefix_length'
273+
)
218274
if vrf_table_id in tables:
275+
if tables[vrf_table_id] != suppress_prefix_len:
276+
raise ConfigError(
277+
f'table {vrf_table_id} has conflicting suppress-prefix-length '
278+
f'with another policy route rule'
279+
)
219280
continue
220-
tables.append(vrf_table_id)
281+
tables[vrf_table_id] = suppress_prefix_len
221282
table_mark = mark_offset - vrf_table_id
222-
cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}')
283+
ip_rule_cmd = (
284+
f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} '
285+
f'table {vrf_table_id}'
286+
)
287+
if suppress_prefix_len is not None:
288+
ip_rule_cmd += (
289+
f' suppress_prefixlength {suppress_prefix_len}'
290+
)
291+
cmd(ip_rule_cmd)
223292

224293
def cleanup_table_marks():
225294
for cmd_str in ['ip', 'ip -6']:

0 commit comments

Comments
 (0)