From 450ca9a9b46d69036af432ddad316d4ddb126085 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 30 Aug 2022 11:46:16 +0200
Subject: firewall: T2199: Refactor firewall + zone-policy, move interfaces
 under firewall node

* Refactor firewall and zone-policy rule creation and cleanup
* Migrate interface firewall values to `firewall interfaces <name> <direction> name/ipv6-name <name>`
* Remove `firewall-interface.py` conf script
---
 src/conf_mode/firewall-interface.py | 186 ----------------------------------
 src/conf_mode/firewall.py           | 193 +++++++-----------------------------
 src/conf_mode/zone_policy.py        |  65 ++++--------
 3 files changed, 57 insertions(+), 387 deletions(-)
 delete mode 100755 src/conf_mode/firewall-interface.py

(limited to 'src/conf_mode')

diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
deleted file mode 100755
index ab1c69259..000000000
--- a/src/conf_mode/firewall-interface.py
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from sys import argv
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import leaf_node_changed
-from vyos.ifconfig import Section
-from vyos.template import render
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import run
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-NAME_PREFIX = 'NAME_'
-NAME6_PREFIX = 'NAME6_'
-
-NFT_CHAINS = {
-    'in': 'VYOS_FW_FORWARD',
-    'out': 'VYOS_FW_FORWARD',
-    'local': 'VYOS_FW_LOCAL'
-}
-NFT6_CHAINS = {
-    'in': 'VYOS_FW6_FORWARD',
-    'out': 'VYOS_FW6_FORWARD',
-    'local': 'VYOS_FW6_LOCAL'
-}
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    ifname = argv[1]
-    ifpath = Section.get_config_path(ifname)
-    if_firewall_path = f'interfaces {ifpath} firewall'
-
-    if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
-
-    if_firewall['ifname'] = ifname
-    if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
-
-    return if_firewall
-
-def verify_chain(table, chain):
-    # Verify firewall applied
-    code = run(f'nft list chain {table} {chain}')
-    return code == 0
-
-def verify(if_firewall):
-    # bail out early - looks like removal from running config
-    if not if_firewall:
-        return None
-
-    for direction in ['in', 'out', 'local']:
-        if direction in if_firewall:
-            if 'name' in if_firewall[direction]:
-                name = if_firewall[direction]['name']
-
-                if 'name' not in if_firewall['firewall']:
-                    raise ConfigError('Firewall name not configured')
-
-                if name not in if_firewall['firewall']['name']:
-                    raise ConfigError(f'Invalid firewall name "{name}"')
-
-                if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'):
-                    raise ConfigError('Firewall did not apply')
-
-            if 'ipv6_name' in if_firewall[direction]:
-                name = if_firewall[direction]['ipv6_name']
-
-                if 'ipv6_name' not in if_firewall['firewall']:
-                    raise ConfigError('Firewall ipv6-name not configured')
-
-                if name not in if_firewall['firewall']['ipv6_name']:
-                    raise ConfigError(f'Invalid firewall ipv6-name "{name}"')
-
-                if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'):
-                    raise ConfigError('Firewall did not apply')
-
-    return None
-
-def generate(if_firewall):
-    return None
-
-def cleanup_rule(table, chain, prefix, ifname, new_name=None):
-    results = cmd(f'nft -a list chain {table} {chain}').split("\n")
-    retval = None
-    for line in results:
-        if f'{prefix}ifname "{ifname}"' in line:
-            if new_name and f'jump {new_name}' in line:
-                # new_name is used to clear rules for any previously referenced chains
-                # returns true when rule exists and doesn't need to be created
-                retval = True
-                continue
-
-            handle_search = re.search('handle (\d+)', line)
-            if handle_search:
-                run(f'nft delete rule {table} {chain} handle {handle_search[1]}')
-    return retval
-
-def state_policy_handle(table, chain):
-    # Find any state-policy rule to ensure interface rules are only inserted afterwards
-    results = cmd(f'nft -a list chain {table} {chain}').split("\n")
-    for line in results:
-        if 'jump VYOS_STATE_POLICY' in line:
-            handle_search = re.search('handle (\d+)', line)
-            if handle_search:
-                return handle_search[1]
-    return None
-
-def apply(if_firewall):
-    ifname = if_firewall['ifname']
-
-    for direction in ['in', 'out', 'local']:
-        chain = NFT_CHAINS[direction]
-        ipv6_chain = NFT6_CHAINS[direction]
-        if_prefix = 'i' if direction in ['in', 'local'] else 'o'
-
-        name = dict_search_args(if_firewall, direction, 'name')
-        if name:
-            rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}')
-
-            if not rule_exists:
-                rule_action = 'insert'
-                rule_prefix = ''
-
-                handle = state_policy_handle('ip filter', chain)
-                if handle:
-                    rule_action = 'add'
-                    rule_prefix = f'position {handle}'
-
-                run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}')
-        else:
-            cleanup_rule('ip filter', chain, if_prefix, ifname)
-
-        ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
-        if ipv6_name:
-            rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}')
-
-            if not rule_exists:
-                rule_action = 'insert'
-                rule_prefix = ''
-
-                handle = state_policy_handle('ip6 filter', ipv6_chain)
-                if handle:
-                    rule_action = 'add'
-                    rule_prefix = f'position {handle}'
-
-                run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}')
-        else:
-            cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname)
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index f0ea1a1e5..86793ba86 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -26,6 +26,7 @@ from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.configdict import node_changed
 from vyos.configdiff import get_config_diff, Diff
+# from vyos.configverify import verify_interface_exists
 from vyos.firewall import geoip_update
 from vyos.firewall import get_ips_domains_dict
 from vyos.firewall import nft_add_set_elements
@@ -38,7 +39,7 @@ from vyos.util import cmd
 from vyos.util import dict_search_args
 from vyos.util import dict_search_recursive
 from vyos.util import process_named_running
-from vyos.util import run
+from vyos.util import rc_cmd
 from vyos.xml import defaults
 from vyos import ConfigError
 from vyos import airbag
@@ -47,7 +48,9 @@ airbag.enable()
 policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'
 
 nftables_conf = '/run/nftables.conf'
-nftables_defines_conf = '/run/nftables_defines.conf'
+
+nftables_zone_conf = '/run/nftables_zone.conf'
+nftables6_zone_conf = '/run/nftables_zone6.conf'
 
 sysfs_config = {
     'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'},
@@ -63,28 +66,6 @@ sysfs_config = {
     'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
 }
 
-NAME_PREFIX = 'NAME_'
-NAME6_PREFIX = 'NAME6_'
-
-preserve_chains = [
-    'INPUT',
-    'FORWARD',
-    'OUTPUT',
-    'VYOS_FW_FORWARD',
-    'VYOS_FW_LOCAL',
-    'VYOS_FW_OUTPUT',
-    'VYOS_POST_FW',
-    'VYOS_FRAG_MARK',
-    'VYOS_FW6_FORWARD',
-    'VYOS_FW6_LOCAL',
-    'VYOS_FW6_OUTPUT',
-    'VYOS_POST_FW6',
-    'VYOS_FRAG6_MARK'
-]
-
-nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']
-nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']
-
 valid_groups = [
     'address_group',
     'domain_group',
@@ -97,16 +78,6 @@ nested_group_types = [
     'port_group', 'ipv6_address_group', 'ipv6_network_group'
 ]
 
-group_set_prefix = {
-    'A_': 'address_group',
-    'A6_': 'ipv6_address_group',
-    'D_': 'domain_group',
-    'M_': 'mac_group',
-    'N_': 'network_group',
-    'N6_': 'ipv6_network_group',
-    'P_': 'port_group'
-}
-
 snmp_change_type = {
     'unknown': 0,
     'add': 1,
@@ -117,22 +88,6 @@ snmp_event_source = 1
 snmp_trap_mib = 'VYATTA-TRAP-MIB'
 snmp_trap_name = 'mgmtEventTrap'
 
-def get_firewall_interfaces(conf):
-    out = {}
-    interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
-    def find_interfaces(iftype_conf, output={}, prefix=''):
-        for ifname, if_conf in iftype_conf.items():
-            if 'firewall' in if_conf:
-                output[prefix + ifname] = if_conf['firewall']
-            for vif in ['vif', 'vif_s', 'vif_c']:
-                if vif in if_conf:
-                    output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.'))
-        return output
-    for iftype, iftype_conf in interfaces.items():
-        out.update(find_interfaces(iftype_conf))
-    return out
-
 def get_firewall_zones(conf):
     used_v4 = []
     used_v6 = []
@@ -159,6 +114,8 @@ def get_firewall_zones(conf):
                 ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name')
                 if ipv6_name:
                     used_v6.append(ipv6_name)
+    else:
+        return None
 
     return {'name': used_v4, 'ipv6_name': used_v6}
 
@@ -232,7 +189,6 @@ def get_config(config=None):
                                                           firewall['ipv6_name'][ipv6_name])
 
     firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
-    firewall['interfaces'] = get_firewall_interfaces(conf)
     firewall['zone_policy'] = get_firewall_zones(conf)
 
     if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
@@ -358,109 +314,42 @@ def verify(firewall):
     for name in ['name', 'ipv6_name']:
         if name in firewall:
             for name_id, name_conf in firewall[name].items():
-                if name_id in preserve_chains:
-                    raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS')
-
-                if name_id.startswith("VZONE"):
-                    raise ConfigError(f'Firewall name "{name_id}" uses reserved prefix')
-
                 if 'rule' in name_conf:
                     for rule_id, rule_conf in name_conf['rule'].items():
                         verify_rule(firewall, rule_conf, name == 'ipv6_name')
 
-    for ifname, if_firewall in firewall['interfaces'].items():
-        for direction in ['in', 'out', 'local']:
-            name = dict_search_args(if_firewall, direction, 'name')
-            ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
+    if 'interface' in firewall:
+        for ifname, if_firewall in firewall['interface'].items():
+            # verify ifname needs to be disabled, dynamic devices come up later
+            # verify_interface_exists(ifname)
 
-            if name and dict_search_args(firewall, 'name', name) == None:
-                raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
+            for direction in ['in', 'out', 'local']:
+                name = dict_search_args(if_firewall, direction, 'name')
+                ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
 
-            if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
-                raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}')
+                if name and dict_search_args(firewall, 'name', name) == None:
+                    raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}')
 
-    for fw_name, used_names in firewall['zone_policy'].items():
-        for name in used_names:
-            if dict_search_args(firewall, fw_name, name) == None:
-                raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy')
+                if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
+                    raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}')
 
-    return None
+    if firewall['zone_policy']:
+        for fw_name, used_names in firewall['zone_policy'].items():
+            for name in used_names:
+                if dict_search_args(firewall, fw_name, name) == None:
+                    raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy')
 
-def cleanup_commands(firewall):
-    commands = []
-    commands_chains = []
-    commands_sets = []
-    for table in ['ip filter', 'ip6 filter']:
-        name_node = 'name' if table == 'ip filter' else 'ipv6_name'
-        chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX
-        state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6'
-        iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains
-
-        geoip_list = []
-        if firewall['geoip_updated']:
-            geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name'
-            geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or []
-
-        json_str = cmd(f'nft -t -j list table {table}')
-        obj = loads(json_str)
-
-        if 'nftables' not in obj:
-            continue
-
-        for item in obj['nftables']:
-            if 'chain' in item:
-                chain = item['chain']['name']
-                if chain in preserve_chains or chain.startswith("VZONE"):
-                    continue
-
-                if chain == state_chain:
-                    command = 'delete' if 'state_policy' not in firewall else 'flush'
-                    commands_chains.append(f'{command} chain {table} {chain}')
-                elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None:
-                    commands.append(f'flush chain {table} {chain}')
-                else:
-                    commands_chains.append(f'delete chain {table} {chain}')
-
-            if 'rule' in item:
-                rule = item['rule']
-                chain = rule['chain']
-                handle = rule['handle']
-
-                if chain in iface_chains:
-                    target, _ = next(dict_search_recursive(rule['expr'], 'target'))
-
-                    if target == state_chain and 'state_policy' not in firewall:
-                        commands.append(f'delete rule {table} {chain} handle {handle}')
-
-                    if target.startswith(chain_prefix):
-                        if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None:
-                            commands.append(f'delete rule {table} {chain} handle {handle}')
-
-            if 'set' in item:
-                set_name = item['set']['name']
-
-                if set_name.startswith('GEOIP_CC_') and set_name in geoip_list:
-                    commands_sets.append(f'delete set {table} {set_name}')
-                    continue
-
-                if set_name.startswith("RECENT_"):
-                    commands_sets.append(f'delete set {table} {set_name}')
-                    continue
-
-                for prefix, group_type in group_set_prefix.items():
-                    if set_name.startswith(prefix):
-                        group_name = set_name.replace(prefix, "", 1)
-                        if dict_search_args(firewall, 'group', group_type, group_name) != None:
-                            commands_sets.append(f'flush set {table} {set_name}')
-                        else:
-                            commands_sets.append(f'delete set {table} {set_name}')
-    return commands + commands_chains + commands_sets
+    return None
 
 def generate(firewall):
     if not os.path.exists(nftables_conf):
         firewall['first_install'] = True
-    else:
-        firewall['cleanup_commands'] = cleanup_commands(firewall)
+
+    if os.path.exists(nftables_zone_conf):
+        firewall['zone_conf'] = nftables_zone_conf
+
+    if os.path.exists(nftables6_zone_conf):
+        firewall['zone6_conf'] = nftables6_zone_conf
 
     render(nftables_conf, 'firewall/nftables.j2', firewall)
     return None
@@ -521,26 +410,21 @@ def post_apply_trap(firewall):
 
                 cmd(base_cmd + ' '.join(objects))
 
-def state_policy_rule_exists():
-    # Determine if state policy rules already exist in nft
-    search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD')
-    return 'VYOS_STATE_POLICY' in search_str
-
 def resync_policy_route():
     # Update policy route as firewall groups were updated
-    tmp = run(policy_route_conf_script)
+    tmp, out = rc_cmd(policy_route_conf_script)
     if tmp > 0:
-        Warning('Failed to re-apply policy route configuration!')
+        Warning(f'Failed to re-apply policy route configuration! {out}')
 
 def apply(firewall):
     if 'first_install' in firewall:
         run('nfct helper add rpc inet tcp')
         run('nfct helper add rpc inet udp')
         run('nfct helper add tns inet tcp')
-
-    install_result = run(f'nft -f {nftables_conf}')
+    
+    install_result, output = rc_cmd(f'nft -f {nftables_conf}')
     if install_result == 1:
-        raise ConfigError('Failed to apply firewall')
+        raise ConfigError(f'Failed to apply firewall: {output}')
 
     # set firewall group domain-group xxx
     if 'group' in firewall:
@@ -563,13 +447,6 @@ def apply(firewall):
         else:
             call('systemctl stop vyos-domain-group-resolve.service')
 
-    if 'state_policy' in firewall and not state_policy_rule_exists():
-        for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
-            cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
-
-        for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']:
-            cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6')
-
     apply_sysfs(firewall)
 
     if firewall['policy_resync']:
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index a52c52706..c6ab4e304 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -21,6 +21,7 @@ from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
+from vyos.configdiff import get_config_diff
 from vyos.template import render
 from vyos.util import cmd
 from vyos.util import dict_search_args
@@ -30,7 +31,9 @@ from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
+firewall_conf_script = '/usr/libexec/vyos/conf_mode/firewall.py'
 nftables_conf = '/run/nftables_zone.conf'
+nftables6_conf = '/run/nftables_zone6.conf'
 
 def get_config(config=None):
     if config:
@@ -47,6 +50,9 @@ def get_config(config=None):
                                                    get_first_key=True,
                                                    no_tag_node_value_mangle=True)
 
+    diff = get_config_diff(conf)
+    zone_policy['firewall_changed'] = diff.is_node_changed(['firewall'])
+
     if 'zone' in zone_policy:
         # We have gathered the dict representation of the CLI, but there are default
         # options which we need to update into the dictionary retrived.
@@ -111,20 +117,12 @@ def verify(zone_policy):
                         raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
 
                     v4_name = dict_search_args(from_conf, 'firewall', 'name')
-                    if v4_name:
-                        if 'name' not in zone_policy['firewall']:
-                            raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
-                        if not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
-                            raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+                    if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
+                        raise ConfigError(f'Firewall name "{v4_name}" does not exist')
 
                     v6_name = dict_search_args(from_conf, 'firewall', 'v6_name')
-                    if v6_name:
-                        if 'ipv6_name' not in zone_policy['firewall']:
-                            raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
-                        if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name):
-                            raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+                    if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name):
+                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
 
     return None
 
@@ -152,37 +150,11 @@ def get_local_from(zone_policy, local_zone_name):
             out[zone] = zone_conf['from'][local_zone_name]
     return out
 
-def cleanup_commands():
-    commands = []
-    for table in ['ip filter', 'ip6 filter']:
-        json_str = cmd(f'nft -t -j list table {table}')
-        obj = loads(json_str)
-        if 'nftables' not in obj:
-            continue
-        for item in obj['nftables']:
-            if 'rule' in item:
-                chain = item['rule']['chain']
-                handle = item['rule']['handle']
-                if 'expr' not in item['rule']:
-                    continue
-                for expr in item['rule']['expr']:
-                    target = dict_search_args(expr, 'jump', 'target')
-                    if not target:
-                        continue
-                    if target.startswith("VZONE") or target.startswith("VYOS_STATE_POLICY"):
-                        commands.append(f'delete rule {table} {chain} handle {handle}')
-        for item in obj['nftables']:
-            if 'chain' in item:
-                if item['chain']['name'].startswith("VZONE"):
-                    chain = item['chain']['name']
-                    commands.append(f'delete chain {table} {chain}')
-    return commands
-
 def generate(zone_policy):
     data = zone_policy or {}
 
-    if os.path.exists(nftables_conf): # Check to see if we've run before
-        data['cleanup_commands'] = cleanup_commands()
+    if not os.path.exists(nftables_conf):
+        data['first_install'] = True
 
     if 'zone' in data:
         for zone, zone_conf in data['zone'].items():
@@ -193,12 +165,19 @@ def generate(zone_policy):
                 zone_conf['from_local'] = get_local_from(data, zone)
 
     render(nftables_conf, 'zone_policy/nftables.j2', data)
+    render(nftables6_conf, 'zone_policy/nftables6.j2', data)
     return None
 
+def update_firewall():
+    # Update firewall to refresh nftables
+    tmp = run(firewall_conf_script)
+    if tmp > 0:
+        Warning('Failed to update firewall configuration!')
+
 def apply(zone_policy):
-    install_result = run(f'nft -f {nftables_conf}')
-    if install_result != 0:
-        raise ConfigError('Failed to apply zone-policy')
+    # If firewall will not update in this commit, we need to call the conf script
+    if not zone_policy['firewall_changed']:
+        update_firewall()
 
     return None
 
-- 
cgit v1.2.3