Skip to content

Commit f72bbd1

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 b96e904 commit f72bbd1

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

data/templates/firewall/nftables-zone.j2

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
iifname { {{ zone[from_zone].member.vrf | quoted_join(",") }} } counter return
5656
{% endif %}
5757
{% endfor %}
58+
{% endif %}
59+
{% if zone_conf.default_firewall is vyos_defined and zone_conf.default_firewall[fw_name] is vyos_defined %}
60+
counter jump NAME{{ suffix }}_{{ zone_conf.default_firewall[fw_name] }}
61+
counter return
5862
{% endif %}
5963
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
6064
}
@@ -71,6 +75,20 @@
7175
oifname { {{ zone[from_zone].member.vrf | quoted_join(",") }} } counter return
7276
{% endif %}
7377
{% endfor %}
78+
{% endif %}
79+
{% if zone_conf.default_local is vyos_defined %}
80+
{% for from_zone, from_conf in zone_conf.default_local.items() if from_conf[fw_name] is vyos_defined %}
81+
{% if 'interface' in zone[from_zone].member %}
82+
oifname { {{ zone[from_zone].member.interface | quoted_join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf[fw_name] }}
83+
oifname { {{ zone[from_zone].member.interface | quoted_join(",") }} } counter return
84+
{% endif %}
85+
{% if 'vrf' in zone[from_zone].member %}
86+
{% for vrf_name in zone[from_zone].member.vrf %}
87+
oifname { "{{ zone[from_zone]['vrf_interfaces'][vrf_name] }}" } counter jump NAME{{ suffix }}_{{ from_conf[fw_name] }}
88+
oifname { "{{ zone[from_zone]['vrf_interfaces'][vrf_name] }}" } counter return
89+
{% endfor %}
90+
{% endif %}
91+
{% endfor %}
7492
{% endif %}
7593
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
7694
}
@@ -103,6 +121,10 @@
103121
{% endif %}
104122
{% endif %}
105123
{% endfor %}
124+
{% endif %}
125+
{% if zone_conf.default_firewall is vyos_defined and zone_conf.default_firewall[fw_name] is vyos_defined %}
126+
counter jump NAME{{ suffix }}_{{ zone_conf.default_firewall[fw_name] }}
127+
counter return
106128
{% endif %}
107129
{{ zone_conf | nft_default_rule('zone_' + zone_name, family) }}
108130
}

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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,50 @@ 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+
self.cli_commit()
1009+
1010+
smoketest_eth0_search = [
1011+
['iifname "eth1"', 'jump NAME_smoketest'],
1012+
['jump NAME_smoketest-default']
1013+
]
1014+
self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth0')
1015+
self.verify_nftables_chain(smoketest_eth0_search, 'ip vyos_filter', 'VZONE_smoketest-eth0')
1016+
1017+
smoketest_eth1_search = [
1018+
['jump NAME_smoketest-default']
1019+
]
1020+
self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth1')
1021+
self.verify_nftables_chain(smoketest_eth1_search, 'ip vyos_filter', 'VZONE_smoketest-eth1')
1022+
1023+
self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-eth2')
1024+
1025+
smoketest_local_in_search = [
1026+
['iifname "eth0"', 'jump NAME_smoketest'],
1027+
['jump NAME_smoketest-default'],
1028+
]
1029+
self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-local_IN')
1030+
self.verify_nftables_chain(smoketest_local_in_search, 'ip vyos_filter', 'VZONE_smoketest-local_IN')
1031+
1032+
smoketest_local_out_search = [
1033+
['iifname "eth0"', 'jump NAME_smoketest'],
1034+
['oifname "eth1"', 'jump NAME_smoketest-default']
1035+
]
1036+
self.verify_nftables_chain_exists('ip vyos_filter', 'VZONE_smoketest-local_OUT')
1037+
self.verify_nftables_chain(smoketest_local_out_search, 'ip vyos_filter', 'VZONE_smoketest-local_OUT')
1038+
9951039
def test_zone_with_vrf(self):
9961040
self.cli_set(['firewall', 'ipv4', 'name', 'ZONE1-to-LOCAL', 'default-action', 'accept'])
9971041
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
@@ -153,12 +153,15 @@ def get_config(config=None):
153153
continue
154154

155155
local_zone_conf['from_local'] = {}
156+
local_zone_conf['default_local'] = {}
156157

157158
for zone, zone_conf in firewall['zone'].items():
158-
if zone == local_zone or 'from' not in zone_conf:
159+
if zone == local_zone:
159160
continue
160-
if local_zone in zone_conf['from']:
161+
if 'from' in zone_conf and local_zone in zone_conf['from']:
161162
local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
163+
elif 'default_firewall' in zone_conf:
164+
local_zone_conf['default_local'][zone] = zone_conf['default_firewall']
162165

163166
set_dependents('conntrack', conf)
164167

@@ -626,6 +629,18 @@ def verify(firewall):
626629
if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
627630
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
628631

632+
if 'default_firewall' in zone_conf:
633+
v4_name = dict_search_args(zone_conf, 'default_firewall', 'name')
634+
if v4_name and not dict_search_args(firewall, 'ipv4', 'name', v4_name):
635+
raise ConfigError(f'Firewall name "{v4_name}" does not exist')
636+
637+
v6_name = dict_search_args(zone_conf, 'default_firewall', 'ipv6_name')
638+
if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
639+
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
640+
641+
if not v4_name and not v6_name:
642+
raise ConfigError('No firewall names specified for default-firewall')
643+
629644
return None
630645

631646
def generate(firewall):

0 commit comments

Comments
 (0)