Skip to content

Commit 51850f5

Browse files
committed
firewall: T7739: Default ruleset for firewall zones
In large networks with many zones where simple allow/deny rules are not sufficient, zones become tedious to manage. Many use cases can be simplified by providing an ability to define a default ruleset for traffic from other zones. This change proposes adding the follwing syntax: set firewall zone <name> default_firewall name <name> set firewall zone <name> default_firewall ipv6_name <name> The proposed behavior is the following: local in: The default firewall ruleset for the local zone will be appended after all from configurations. local out: If a non-local zone does not have a from local ruleset but does have a default_firewall ruleset, the default_firewall ruleset will be appended using oifname forward: The default firewall ruleset for the zone will be appended after all from configurations To keep the behavior consistent with from ruleset configurations, a return is appended after the default_firewall ruleset. The proposed behavior differs slightly from the default_policy configuration for the local out chains. The default_policy applied in the out templates comes from the local zone, not the actual outbound zone. The proposed change does not amend this, but does make default_firewall logically consistent with the intent of the out rules.
1 parent 69cd484 commit 51850f5

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

data/templates/firewall/nftables-zone.j2

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757
iifname { {{ zone[from_zone].member.vrf | quoted_join(",") }} } counter return
5858
{% endif %}
5959
{% endfor %}
60+
{% endif %}
61+
{% if zone_conf.default_firewall is vyos_defined and zone_conf.default_firewall[fw_name] is vyos_defined %}
62+
counter jump NAME{{ suffix }}_{{ zone_conf.default_firewall[fw_name] }}
63+
counter return
6064
{% endif %}
6165
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
6266
}
@@ -75,6 +79,20 @@
7579
{% endfor %}
7680
{% endif %}
7781
{% endfor %}
82+
{% endif %}
83+
{% if zone_conf.default_local is vyos_defined %}
84+
{% for from_zone, from_conf in zone_conf.default_local.items() if from_conf[fw_name] is vyos_defined %}
85+
{% if 'interface' in zone[from_zone].member %}
86+
oifname { {{ zone[from_zone].member.interface | quoted_join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf[fw_name] }}
87+
oifname { {{ zone[from_zone].member.interface | quoted_join(",") }} } counter return
88+
{% endif %}
89+
{% if 'vrf' in zone[from_zone].member %}
90+
{% for vrf_name in zone[from_zone].member.vrf %}
91+
oifname { "{{ zone[from_zone]['vrf_interfaces'][vrf_name] }}" } counter jump NAME{{ suffix }}_{{ from_conf[fw_name] }}
92+
oifname { "{{ zone[from_zone]['vrf_interfaces'][vrf_name] }}" } counter return
93+
{% endfor %}
94+
{% endif %}
95+
{% endfor %}
7896
{% endif %}
7997
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
8098
}
@@ -107,6 +125,10 @@
107125
{% endif %}
108126
{% endif %}
109127
{% endfor %}
128+
{% endif %}
129+
{% if zone_conf.default_firewall is vyos_defined and zone_conf.default_firewall[fw_name] is vyos_defined %}
130+
counter jump NAME{{ suffix }}_{{ zone_conf.default_firewall[fw_name] }}
131+
counter return
110132
{% endif %}
111133
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
112134
}

interface-definitions/firewall.xml.in

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,29 @@
428428
</properties>
429429
<defaultValue>drop</defaultValue>
430430
</leafNode>
431+
<node name="default-firewall">
432+
<properties>
433+
<help>Default firewall rules for traffic coming into this zone</help>
434+
</properties>
435+
<children>
436+
<leafNode name="ipv6-name">
437+
<properties>
438+
<help>IPv6 firewall ruleset</help>
439+
<completionHelp>
440+
<path>firewall ipv6 name</path>
441+
</completionHelp>
442+
</properties>
443+
</leafNode>
444+
<leafNode name="name">
445+
<properties>
446+
<help>IPv4 firewall ruleset</help>
447+
<completionHelp>
448+
<path>firewall ipv4 name</path>
449+
</completionHelp>
450+
</properties>
451+
</leafNode>
452+
</children>
453+
</node>
431454
<tagNode name="from">
432455
<properties>
433456
<help>Zone from which to filter traffic</help>

smoketest/scripts/cli/test_firewall.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,47 @@ def test_zone_basic(self):
992992
self.verify_nftables(nftables_search, 'ip vyos_filter')
993993
self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
994994

995+
def test_zone_with_default_firewall(self):
996+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop'])
997+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-default', 'default-action', 'drop'])
998+
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0'])
999+
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-eth1', 'firewall', 'name', 'smoketest'])
1000+
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
1001+
self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'default-firewall', 'name', 'smoketest-default'])
1002+
self.cli_set(['firewall', 'zone', 'smoketest-eth1', 'member', 'interface', 'eth1'])
1003+
self.cli_set(['firewall', 'zone', 'smoketest-eth1', 'default-firewall', 'name', 'smoketest-default'])
1004+
self.cli_set(['firewall', 'zone', 'smoketest-eth2', 'member', 'interface', 'eth2'])
1005+
self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone'])
1006+
self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest'])
1007+
self.cli_set(['firewall', 'zone', 'smoketest-local', 'default-firewall', 'name', 'smoketest-default'])
1008+
1009+
self.cli_commit()
1010+
1011+
nftables_search = [
1012+
['chain NAME_smoketest'],
1013+
['chain NAME_smoketest-default'],
1014+
['chain VYOS_ZONE_FORWARD'],
1015+
['type filter hook forward priority filter + 1'],
1016+
['chain VYOS_ZONE_OUTPUT'],
1017+
['type filter hook output priority filter + 1'],
1018+
['chain VYOS_ZONE_LOCAL'],
1019+
['type filter hook input priority filter + 1'],
1020+
['chain VZONE_smoketest-eth0'],
1021+
['iifname "eth1"', 'jump NAME_smoketest'],
1022+
['jump NAME_smoketest-default'],
1023+
['chain VZONE_smoketest-eth1'],
1024+
['jump NAME_smoketest-default'],
1025+
['chain VZONE_smoketest-eth2'],
1026+
['chain VZONE_smoketest-local_IN'],
1027+
['iifname "eth0"', 'jump NAME_smoketest'],
1028+
['jump NAME_smoketest-default'],
1029+
['chain VZONE_smoketest-local_OUT'],
1030+
['iifname "eth0"', 'jump NAME_smoketest'],
1031+
['oifname "eth1"', 'jump NAME_smoketest-default']
1032+
]
1033+
1034+
self.verify_nftables(nftables_search, 'ip vyos_filter')
1035+
9951036
def test_zone_with_vrf(self):
9961037
self.cli_set(['firewall', 'ipv4', 'name', 'ZONE1-to-LOCAL', 'default-action', 'accept'])
9971038
self.cli_set(['firewall', 'ipv4', 'name', 'ZONE2_to_ZONE1', 'default-action', 'continue'])

src/conf_mode/firewall.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,15 @@ def get_config(config=None):
152152
continue
153153

154154
local_zone_conf['from_local'] = {}
155+
local_zone_conf['default_local'] = {}
155156

156157
for zone, zone_conf in firewall['zone'].items():
157-
if zone == local_zone or 'from' not in zone_conf:
158+
if zone == local_zone:
158159
continue
159-
if local_zone in zone_conf['from']:
160+
if 'from' in zone_conf and local_zone in zone_conf['from']:
160161
local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
162+
elif 'default_firewall' in zone_conf:
163+
local_zone_conf['default_local'][zone] = zone_conf['default_firewall']
161164

162165
set_dependents('conntrack', conf)
163166

@@ -553,6 +556,18 @@ def verify(firewall):
553556
if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
554557
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
555558

559+
if 'default_firewall' in zone_conf:
560+
v4_name = dict_search_args(zone_conf, 'default_firewall', 'name')
561+
if v4_name and not dict_search_args(firewall, 'ipv4', 'name', v4_name):
562+
raise ConfigError(f'Firewall name "{v4_name}" does not exist')
563+
564+
v6_name = dict_search_args(zone_conf, 'default_firewall', 'ipv6_name')
565+
if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
566+
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
567+
568+
if not v4_name and not v6_name:
569+
raise ConfigError('No firewall names specified for default-firewall')
570+
556571
return None
557572

558573
def generate(firewall):

0 commit comments

Comments
 (0)