diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index a35143870..67473da8e 100755
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -1,449 +1,449 @@
 #!/usr/sbin/nft -f
 
 {% import 'firewall/nftables-defines.j2' as group_tmpl %}
 {% import 'firewall/nftables-bridge.j2' as bridge_tmpl %}
 {% import 'firewall/nftables-offload.j2' as offload_tmpl %}
 {% import 'firewall/nftables-zone.j2' as zone_tmpl %}
 
 flush chain raw vyos_global_rpfilter
 flush chain ip6 raw vyos_global_rpfilter
 
 table raw {
     chain vyos_global_rpfilter {
 {% if global_options.source_validation is vyos_defined('loose') %}
         fib saddr oif 0 counter drop
 {% elif global_options.source_validation is vyos_defined('strict') %}
         fib saddr . iif oif 0 counter drop
 {% endif %}
         return
     }
 }
 
 table ip6 raw {
     chain vyos_global_rpfilter {
 {% if global_options.ipv6_source_validation is vyos_defined('loose') %}
         fib saddr oif 0 counter drop
 {% elif global_options.ipv6_source_validation is vyos_defined('strict') %}
         fib saddr . iif oif 0 counter drop
 {% endif %}
         return
     }
 }
 
 {% if first_install is not vyos_defined %}
 delete table ip vyos_filter
 {% endif %}
 table ip vyos_filter {
 {% if ipv4 is vyos_defined %}
 {%     if flowtable is vyos_defined %}
 {%         for name, flowtable_conf in flowtable.items() %}
 {{ offload_tmpl.flowtable(name, flowtable_conf) }}
 {%         endfor %}
 {%     endif %}
 
 {%     set ns = namespace(sets=[]) %}
 {%     if ipv4.forward is vyos_defined %}
 {%         for prior, conf in ipv4.forward.items() %}
     chain VYOS_FORWARD_{{ prior }} {
         type filter hook forward priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('FWD', prior, rule_id) }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('FWD-' + prior, 'ipv4') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv4.input is vyos_defined %}
 {%         for prior, conf in ipv4.input.items() %}
     chain VYOS_INPUT_{{ prior }} {
         type filter hook input priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('INP',prior, rule_id) }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('INP-' + prior, 'ipv4') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv4.output is vyos_defined %}
 {%         for prior, conf in ipv4.output.items() %}
     chain VYOS_OUTPUT_{{ prior }} {
         type filter hook output priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined and prior == 'filter' %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('OUT', prior, rule_id) }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['OUT_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('OUT-' + prior, 'ipv4') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv4.prerouting is vyos_defined %}
 {%         for prior, conf in ipv4.prerouting.items() %}
     chain VYOS_PREROUTING_{{ prior }} {
         type filter hook prerouting priority {{ prior }}; policy accept;
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('PRE', prior, rule_id) }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['PRE_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('PRE-' + prior, 'ipv4') }}
     }
 {%         endfor %}
 {%     endif %}
 
     chain VYOS_FRAG_MARK {
         type filter hook prerouting priority -450; policy accept;
         ip frag-off & 0x3fff != 0 meta mark set 0xffff1 return
     }
 
 {%     if ipv4.name is vyos_defined %}
 {%         for name_text, conf in ipv4.name.items() %}
     chain NAME_{{ name_text }} {
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('NAM', name_text, rule_id) }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('NAM-' + name_text, 'ipv4') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     for set_name in ns.sets %}
     set RECENT_{{ set_name }} {
         type ipv4_addr
         size 65535
         flags dynamic
     }
 {%     endfor %}
 {%     for set_name in ip_fqdn %}
     set FQDN_{{ set_name }} {
         type ipv4_addr
         flags interval
     }
 {%     endfor %}
 {%     if geoip_updated.name is vyos_defined %}
 {%         for setname in geoip_updated.name %}
     set {{ setname }} {
         type ipv4_addr
         flags interval
     }
 {%         endfor %}
 {%     endif %}
 {% endif %}
 {{ group_tmpl.groups(group, False, True) }}
 
 {% if zone is vyos_defined %}
 {{ zone_tmpl.zone_chains(zone, False, global_options.state_policy is vyos_defined) }}
 {% endif %}
 {% if global_options.state_policy is vyos_defined %}
     chain VYOS_STATE_POLICY {
 {%     if global_options.state_policy.established is vyos_defined %}
         {{ global_options.state_policy.established | nft_state_policy('established') }}
 {%     endif %}
 {%     if global_options.state_policy.invalid is vyos_defined %}
         {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
 {%     endif %}
 {%     if global_options.state_policy.related is vyos_defined %}
         {{ global_options.state_policy.related | nft_state_policy('related') }}
 {%     endif %}
         return
     }
 {% endif %}
 }
 
 {% if first_install is not vyos_defined %}
 delete table ip6 vyos_filter
 {% endif %}
 table ip6 vyos_filter {
 {% if ipv6 is vyos_defined %}
 {%     if flowtable is vyos_defined %}
 {%         for name, flowtable_conf in flowtable.items() %}
 {{ offload_tmpl.flowtable(name, flowtable_conf) }}
 {%         endfor %}
 {%     endif %}
 
 {%     set ns = namespace(sets=[]) %}
 {%     if ipv6.forward is vyos_defined %}
 {%         for prior, conf in ipv6.forward.items() %}
     chain VYOS_IPV6_FORWARD_{{ prior }} {
         type filter hook forward priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY6
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('FWD', prior, rule_id, 'ip6') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('FWD-' + prior, 'ipv6') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv6.input is vyos_defined %}
 {%         for prior, conf in ipv6.input.items() %}
     chain VYOS_IPV6_INPUT_{{ prior }} {
         type filter hook input priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY6
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('INP', prior, rule_id, 'ip6') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('INP-' + prior, 'ipv6') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv6.output is vyos_defined %}
 {%         for prior, conf in ipv6.output.items() %}
     chain VYOS_IPV6_OUTPUT_{{ prior }} {
         type filter hook output priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined and prior == 'filter' %}
         jump VYOS_STATE_POLICY6
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('OUT', prior, rule_id, 'ip6') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['OUT_ ' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('OUT-' + prior, 'ipv6') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if ipv6.prerouting is vyos_defined %}
 {%         for prior, conf in ipv6.prerouting.items() %}
     chain VYOS_IPV6_PREROUTING_{{ prior }} {
         type filter hook prerouting priority {{ prior }}; policy accept;
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('PRE', prior, rule_id, 'ip6') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['PRE_' + prior + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('PRE-' + prior, 'ipv6') }}
     }
 {%         endfor %}
 {%     endif %}
     chain VYOS_FRAG6_MARK {
         type filter hook prerouting priority -450; policy accept;
         exthdr frag exists meta mark set 0xffff1 return
     }
 
 {%     if ipv6.name is vyos_defined %}
 {%         for name_text, conf in ipv6.name.items() %}
     chain NAME6_{{ name_text }} {
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'ip6') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('NAM-' + name_text, 'ipv6') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     for set_name in ns.sets %}
     set RECENT6_{{ set_name }} {
         type ipv6_addr
         size 65535
         flags dynamic
     }
 {%     endfor %}
 {%     for set_name in ip6_fqdn %}
     set FQDN_{{ set_name }} {
         type ipv6_addr
         flags interval
     }
 {%     endfor %}
 {%     if geoip_updated.ipv6_name is vyos_defined %}
 {%         for setname in geoip_updated.ipv6_name %}
     set {{ setname }} {
         type ipv6_addr
         flags interval
     }
 {%         endfor %}
 {%     endif %}
 {% endif %}
 {{ group_tmpl.groups(group, True, True) }}
 {% if zone is vyos_defined %}
 {{ zone_tmpl.zone_chains(zone, True, global_options.state_policy is vyos_defined) }}
 {% endif %}
 {% if global_options.state_policy is vyos_defined %}
     chain VYOS_STATE_POLICY6 {
 {%     if global_options.state_policy.established is vyos_defined %}
         {{ global_options.state_policy.established | nft_state_policy('established') }}
 {%     endif %}
 {%     if global_options.state_policy.invalid is vyos_defined %}
         {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
 {%     endif %}
 {%     if global_options.state_policy.related is vyos_defined %}
         {{ global_options.state_policy.related | nft_state_policy('related') }}
 {%     endif %}
         return
     }
 {% endif %}
 }
 
 ## Bridge Firewall
 {% if first_install is not vyos_defined %}
 delete table bridge vyos_filter
 {% endif %}
 table bridge vyos_filter {
 {% if bridge is vyos_defined %}
 {%     if bridge.forward is vyos_defined %}
 {%         for prior, conf in bridge.forward.items() %}
     chain VYOS_FORWARD_{{ prior }} {
         type filter hook forward priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('FWD', prior, rule_id, 'bri') }}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('FWD-' + prior, 'bri') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if bridge.input is vyos_defined %}
 {%         for prior, conf in bridge.input.items() %}
     chain VYOS_INPUT_{{ prior }} {
         type filter hook input priority {{ prior }}; policy accept;
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('INP', prior, rule_id, 'bri') }}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('INP-' + prior, 'bri') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if bridge.output is vyos_defined %}
 {%         for prior, conf in bridge.output.items() %}
     chain VYOS_OUTPUT_{{ prior }} {
         type filter hook output priority {{ prior }}; policy accept;
 {%             if global_options.apply_to_bridged_traffic is vyos_defined %}
 {%                 if 'invalid_connections' in global_options.apply_to_bridged_traffic %}
         ct state invalid udp sport 67 udp dport 68 counter accept
         ct state invalid ether type arp counter accept
         ct state invalid ether type 0x8864 counter accept
 {%                 endif %}
 {%             endif %}
 {%             if global_options.state_policy is vyos_defined %}
         jump VYOS_STATE_POLICY
 {%             endif %}
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('OUT', prior, rule_id, 'bri') }}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('OUT-' + prior, 'bri') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if bridge.prerouting is vyos_defined %}
 {%         for prior, conf in bridge.prerouting.items() %}
     chain VYOS_PREROUTING_{{ prior }} {
         type filter hook prerouting priority {{ prior }}; policy accept;
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('PRE', prior, rule_id, 'bri') }}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('PRE-' + prior, 'bri') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {%     if bridge.name is vyos_defined %}
 {%         for name_text, conf in bridge.name.items() %}
     chain NAME_{{ name_text }} {
 {%             if conf.rule is vyos_defined %}
 {%                 for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
         {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'bri') }}
 {%                     if rule_conf.recent is vyos_defined %}
 {%                         set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %}
 {%                     endif %}
 {%                 endfor %}
 {%             endif %}
         {{ conf | nft_default_rule('NAM-' + name_text, 'bri') }}
     }
 {%         endfor %}
 {%     endif %}
 
 {% endif %}
 {{ group_tmpl.groups(group, False, False) }}
 {{ group_tmpl.groups(group, True, False) }}
 
 {% if global_options.state_policy is vyos_defined %}
     chain VYOS_STATE_POLICY {
 {%     if global_options.state_policy.established is vyos_defined %}
-        {{ global_options.state_policy.established | nft_state_policy('established') }}
+        {{ global_options.state_policy.established | nft_state_policy('established', bridge=True) }}
 {%     endif %}
 {%     if global_options.state_policy.invalid is vyos_defined %}
-        {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+        {{ global_options.state_policy.invalid | nft_state_policy('invalid', bridge=True) }}
 {%     endif %}
 {%     if global_options.state_policy.related is vyos_defined %}
-        {{ global_options.state_policy.related | nft_state_policy('related') }}
+        {{ global_options.state_policy.related | nft_state_policy('related', bridge=True) }}
 {%     endif %}
         return
     }
 {% endif %}
 }
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 7ba608b32..e75db1a8d 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,995 +1,1000 @@
 # Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library 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
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import functools
 import os
 
 from jinja2 import Environment
 from jinja2 import FileSystemLoader
 from jinja2 import ChainableUndefined
 from vyos.defaults import directories
 from vyos.utils.dict import dict_search_args
 from vyos.utils.file import makedir
 from vyos.utils.permission import chmod
 from vyos.utils.permission import chown
 
 # We use a mutable global variable for the default template directory
 # to make it possible to call scripts from this repository
 # outside of live VyOS systems.
 # If something (like the image build scripts)
 # want to call a script, they can modify the default location
 # to the repository path.
 DEFAULT_TEMPLATE_DIR = directories["templates"]
 
 # Holds template filters registered via register_filter()
 _FILTERS = {}
 _TESTS = {}
 
 # reuse Environments with identical settings to improve performance
 @functools.lru_cache(maxsize=2)
 def _get_environment(location=None):
     from os import getenv
 
     if location is None:
         loc_loader=FileSystemLoader(DEFAULT_TEMPLATE_DIR)
     else:
         loc_loader=FileSystemLoader(location)
     env = Environment(
         # Don't check if template files were modified upon re-rendering
         auto_reload=False,
         # Cache up to this number of templates for quick re-rendering
         cache_size=100,
         loader=loc_loader,
         trim_blocks=True,
         undefined=ChainableUndefined,
         extensions=['jinja2.ext.loopcontrols']
     )
     env.filters.update(_FILTERS)
     env.tests.update(_TESTS)
     return env
 
 
 def register_filter(name, func=None):
     """Register a function to be available as filter in templates under given name.
 
     It can also be used as a decorator, see below in this module for examples.
 
     :raise RuntimeError:
         when trying to register a filter after a template has been rendered already
     :raise ValueError: when trying to register a name which was taken already
     """
     if func is None:
         return functools.partial(register_filter, name)
     if _get_environment.cache_info().currsize:
         raise RuntimeError(
             "Filters can only be registered before rendering the first template"
         )
     if name in _FILTERS:
         raise ValueError(f"A filter with name {name!r} was registered already")
     _FILTERS[name] = func
     return func
 
 def register_test(name, func=None):
     """Register a function to be available as test in templates under given name.
 
     It can also be used as a decorator, see below in this module for examples.
 
     :raise RuntimeError:
         when trying to register a test after a template has been rendered already
     :raise ValueError: when trying to register a name which was taken already
     """
     if func is None:
         return functools.partial(register_test, name)
     if _get_environment.cache_info().currsize:
         raise RuntimeError(
             "Tests can only be registered before rendering the first template"
             )
     if name in _TESTS:
         raise ValueError(f"A test with name {name!r} was registered already")
     _TESTS[name] = func
     return func
 
 
 def render_to_string(template, content, formater=None, location=None):
     """Render a template from the template directory, raise on any errors.
 
     :param template: the path to the template relative to the template folder
     :param content: the dictionary of variables to put into rendering context
     :param formater:
         if given, it has to be a callable the rendered string is passed through
 
     The parsed template files are cached, so rendering the same file multiple times
     does not cause as too much overhead.
     If used everywhere, it could be changed to load the template from Python
     environment variables from an importable Python module generated when the Debian
     package is build (recovering the load time and overhead caused by having the
     file out of the code).
     """
     template = _get_environment(location).get_template(template)
     rendered = template.render(content)
     if formater is not None:
         rendered = formater(rendered)
     return rendered
 
 
 def render(
     destination,
     template,
     content,
     formater=None,
     permission=None,
     user=None,
     group=None,
     location=None,
 ):
     """Render a template from the template directory to a file, raise on any errors.
 
     :param destination: path to the file to save the rendered template in
     :param permission: permission bitmask to set for the output file
     :param user: user to own the output file
     :param group: group to own the output file
 
     All other parameters are as for :func:`render_to_string`.
     """
     # Create the directory if it does not exist
     folder = os.path.dirname(destination)
     makedir(folder, user, group)
 
     # As we are opening the file with 'w', we are performing the rendering before
     # calling open() to not accidentally erase the file if rendering fails
     rendered = render_to_string(template, content, formater, location)
 
     # Write to file
     with open(destination, "w") as file:
         chmod(file.fileno(), permission)
         chown(file.fileno(), user, group)
         file.write(rendered)
 
 
 ##################################
 # Custom template filters follow #
 ##################################
 @register_filter('force_to_list')
 def force_to_list(value):
     """ Convert scalars to single-item lists and leave lists untouched """
     if isinstance(value, list):
         return value
     else:
         return [value]
 
 @register_filter('seconds_to_human')
 def seconds_to_human(seconds, separator=""):
     """ Convert seconds to human-readable values like 1d6h15m23s """
     from vyos.utils.convert import seconds_to_human
     return seconds_to_human(seconds, separator=separator)
 
 @register_filter('bytes_to_human')
 def bytes_to_human(bytes, initial_exponent=0, precision=2):
     """ Convert bytes to human-readable values like 1.44M """
     from vyos.utils.convert import bytes_to_human
     return bytes_to_human(bytes, initial_exponent=initial_exponent, precision=precision)
 
 @register_filter('human_to_bytes')
 def human_to_bytes(value):
     """ Convert a data amount with a unit suffix to bytes, like 2K to 2048 """
     from vyos.utils.convert import human_to_bytes
     return human_to_bytes(value)
 
 @register_filter('ip_from_cidr')
 def ip_from_cidr(prefix):
     """ Take an IPv4/IPv6 CIDR host and strip cidr mask.
     Example:
     192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1
     """
     from ipaddress import ip_interface
     return str(ip_interface(prefix).ip)
 
 @register_filter('address_from_cidr')
 def address_from_cidr(prefix):
     """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
     Example:
     192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
     """
     from ipaddress import ip_network
     return str(ip_network(prefix).network_address)
 
 @register_filter('bracketize_ipv6')
 def bracketize_ipv6(address):
     """ Place a passed IPv6 address into [] brackets, do nothing for IPv4 """
     if is_ipv6(address):
         return f'[{address}]'
     return address
 
 @register_filter('dot_colon_to_dash')
 def dot_colon_to_dash(text):
     """ Replace dot and colon to dash for string
     Example:
     192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1
     """
     text = text.replace(":", "-")
     text = text.replace(".", "-")
     return text
 
 @register_filter('generate_uuid4')
 def generate_uuid4(text):
     """ Generate random unique ID
     Example:
       % uuid4()
       UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5')
     """
     from uuid import uuid4
     return uuid4()
 
 @register_filter('netmask_from_cidr')
 def netmask_from_cidr(prefix):
     """ Take CIDR prefix and convert the prefix length to a "subnet mask".
     Example:
       - 192.0.2.0/24 -> 255.255.255.0
       - 2001:db8::/48 -> ffff:ffff:ffff::
     """
     from ipaddress import ip_network
     return str(ip_network(prefix).netmask)
 
 @register_filter('netmask_from_ipv4')
 def netmask_from_ipv4(address):
     """ Take IP address and search all attached interface IP addresses for the
     given one. After address has been found, return the associated netmask.
 
     Example:
       - 172.18.201.10 -> 255.255.255.128
     """
     from netifaces import interfaces
     from netifaces import ifaddresses
     from netifaces import AF_INET
     for interface in interfaces():
         tmp = ifaddresses(interface)
         if AF_INET in tmp:
             for af_addr in tmp[AF_INET]:
                 if 'addr' in af_addr:
                     if af_addr['addr'] == address:
                         return af_addr['netmask']
 
     raise ValueError
 
 @register_filter('is_ip_network')
 def is_ip_network(addr):
     """ Take IP(v4/v6) address and validate if the passed argument is a network
     or a host address.
 
     Example:
       - 192.0.2.0          -> False
       - 192.0.2.10/24      -> False
       - 192.0.2.0/24       -> True
       - 2001:db8::         -> False
       - 2001:db8::100      -> False
       - 2001:db8::/48      -> True
       - 2001:db8:1000::/64 -> True
     """
     try:
         from ipaddress import ip_network
         # input variables must contain a / to indicate its CIDR notation
         if len(addr.split('/')) != 2:
             raise ValueError()
         ip_network(addr)
         return True
     except:
         return False
 
 @register_filter('network_from_ipv4')
 def network_from_ipv4(address):
     """ Take IP address and search all attached interface IP addresses for the
     given one. After address has been found, return the associated network
     address.
 
     Example:
       - 172.18.201.10 has mask 255.255.255.128 -> network is 172.18.201.0
     """
     netmask = netmask_from_ipv4(address)
     from ipaddress import ip_interface
     cidr_prefix = ip_interface(f'{address}/{netmask}').network
     return address_from_cidr(cidr_prefix)
 
 @register_filter('is_interface')
 def is_interface(interface):
     """ Check if parameter is a valid local interface name """
     from vyos.utils.network import interface_exists
     return interface_exists(interface)
 
 @register_filter('is_ip')
 def is_ip(addr):
     """ Check addr if it is an IPv4 or IPv6 address """
     return is_ipv4(addr) or is_ipv6(addr)
 
 @register_filter('is_ipv4')
 def is_ipv4(text):
     """ Filter IP address, return True on IPv4 address, False otherwise """
     from ipaddress import ip_interface
     try: return ip_interface(text).version == 4
     except: return False
 
 @register_filter('is_ipv6')
 def is_ipv6(text):
     """ Filter IP address, return True on IPv6 address, False otherwise """
     from ipaddress import ip_interface
     try: return ip_interface(text).version == 6
     except: return False
 
 @register_filter('first_host_address')
 def first_host_address(prefix):
     """ Return first usable (host) IP address from given prefix.
     Example:
       - 10.0.0.0/24 -> 10.0.0.1
       - 2001:db8::/64 -> 2001:db8::
     """
     from ipaddress import ip_interface
     tmp = ip_interface(prefix).network
     return str(tmp.network_address +1)
 
 @register_filter('last_host_address')
 def last_host_address(text):
     """ Return first usable IP address from given prefix.
     Example:
       - 10.0.0.0/24 -> 10.0.0.254
       - 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff
     """
     from ipaddress import ip_interface
     from ipaddress import IPv4Network
     from ipaddress import IPv6Network
 
     addr = ip_interface(text)
     if addr.version == 4:
         return str(IPv4Network(addr).broadcast_address - 1)
 
     return str(IPv6Network(addr).broadcast_address)
 
 @register_filter('inc_ip')
 def inc_ip(address, increment):
     """ Increment given IP address by 'increment'
 
     Example (inc by 2):
       - 10.0.0.0/24 -> 10.0.0.2
       - 2001:db8::/64 -> 2001:db8::2
     """
     from ipaddress import ip_interface
     return str(ip_interface(address).ip + int(increment))
 
 @register_filter('dec_ip')
 def dec_ip(address, decrement):
     """ Decrement given IP address by 'decrement'
 
     Example (inc by 2):
       - 10.0.0.0/24 -> 10.0.0.2
       - 2001:db8::/64 -> 2001:db8::2
     """
     from ipaddress import ip_interface
     return str(ip_interface(address).ip - int(decrement))
 
 @register_filter('compare_netmask')
 def compare_netmask(netmask1, netmask2):
     """
     Compare two IP netmask if they have the exact same size.
 
     compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True
     compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False
     """
     from ipaddress import ip_network
     try:
         return ip_network(netmask1).netmask == ip_network(netmask2).netmask
     except:
         return False
 
 @register_filter('isc_static_route')
 def isc_static_route(subnet, router):
     # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
     # Option format is:
     # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
     # where bytes with the value 0 are omitted.
     from ipaddress import ip_network
     net = ip_network(subnet)
     # add netmask
     string = str(net.prefixlen) + ','
     # add network bytes
     if net.prefixlen:
         width = net.prefixlen // 8
         if net.prefixlen % 8:
             width += 1
         string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
 
     # add router bytes
     string += ','.join(router.split('.'))
 
     return string
 
 @register_filter('is_file')
 def is_file(filename):
     if os.path.exists(filename):
         return os.path.isfile(filename)
     return False
 
 @register_filter('get_dhcp_router')
 def get_dhcp_router(interface):
     """ Static routes can point to a router received by a DHCP reply. This
     helper is used to get the current default router from the DHCP reply.
 
     Returns False of no router is found, returns the IP address as string if
     a router is found.
     """
     lease_file = directories['isc_dhclient_dir'] + f'/dhclient_{interface}.leases'
     if not os.path.exists(lease_file):
         return None
 
     from vyos.utils.file import read_file
     for line in read_file(lease_file).splitlines():
         if 'option routers' in line:
             (_, _, address) = line.split()
             return address.rstrip(';')
 
 @register_filter('natural_sort')
 def natural_sort(iterable):
     import re
     from jinja2.runtime import Undefined
 
     if isinstance(iterable, Undefined) or iterable is None:
         return list()
 
     def convert(text):
         return int(text) if text.isdigit() else text.lower()
     def alphanum_key(key):
         return [convert(c) for c in re.split('([0-9]+)', str(key))]
 
     return sorted(iterable, key=alphanum_key)
 
 @register_filter('get_ipv4')
 def get_ipv4(interface):
     """ Get interface IPv4 addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr_v4()
 
 @register_filter('get_ipv6')
 def get_ipv6(interface):
     """ Get interface IPv6 addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr_v6()
 
 @register_filter('get_ip')
 def get_ip(interface):
     """ Get interface IP addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr()
 
 def get_first_ike_dh_group(ike_group):
     if ike_group and 'proposal' in ike_group:
         for priority, proposal in ike_group['proposal'].items():
             if 'dh_group' in proposal:
                 return 'dh-group' + proposal['dh_group']
     return 'dh-group2' # Fallback on dh-group2
 
 @register_filter('get_esp_ike_cipher')
 def get_esp_ike_cipher(group_config, ike_group=None):
     pfs_lut = {
         'dh-group1'  : 'modp768',
         'dh-group2'  : 'modp1024',
         'dh-group5'  : 'modp1536',
         'dh-group14' : 'modp2048',
         'dh-group15' : 'modp3072',
         'dh-group16' : 'modp4096',
         'dh-group17' : 'modp6144',
         'dh-group18' : 'modp8192',
         'dh-group19' : 'ecp256',
         'dh-group20' : 'ecp384',
         'dh-group21' : 'ecp521',
         'dh-group22' : 'modp1024s160',
         'dh-group23' : 'modp2048s224',
         'dh-group24' : 'modp2048s256',
         'dh-group25' : 'ecp192',
         'dh-group26' : 'ecp224',
         'dh-group27' : 'ecp224bp',
         'dh-group28' : 'ecp256bp',
         'dh-group29' : 'ecp384bp',
         'dh-group30' : 'ecp512bp',
         'dh-group31' : 'curve25519',
         'dh-group32' : 'curve448'
     }
 
     ciphers = []
     if 'proposal' in group_config:
         for priority, proposal in group_config['proposal'].items():
             # both encryption and hash need to be specified for a proposal
             if not {'encryption', 'hash'} <= set(proposal):
                 continue
 
             tmp = '{encryption}-{hash}'.format(**proposal)
             if 'prf' in proposal:
                 tmp += '-' + proposal['prf']
             if 'dh_group' in proposal:
                 tmp += '-' + pfs_lut[ 'dh-group' +  proposal['dh_group'] ]
             elif 'pfs' in group_config and group_config['pfs'] != 'disable':
                 group = group_config['pfs']
                 if group_config['pfs'] == 'enable':
                     group = get_first_ike_dh_group(ike_group)
                 tmp += '-' + pfs_lut[group]
 
             ciphers.append(tmp)
     return ciphers
 
 @register_filter('get_uuid')
 def get_uuid(seed):
     """ Get interface IP addresses"""
     if seed:
         from hashlib import md5
         from uuid import UUID
         tmp = md5()
         tmp.update(seed.encode('utf-8'))
         return str(UUID(tmp.hexdigest()))
     else:
         from uuid import uuid1
         return uuid1()
 
 openvpn_translate = {
     'des': 'des-cbc',
     '3des': 'des-ede3-cbc',
     'bf128': 'bf-cbc',
     'bf256': 'bf-cbc',
     'aes128gcm': 'aes-128-gcm',
     'aes128': 'aes-128-cbc',
     'aes192gcm': 'aes-192-gcm',
     'aes192': 'aes-192-cbc',
     'aes256gcm': 'aes-256-gcm',
     'aes256': 'aes-256-cbc'
 }
 
 @register_filter('openvpn_cipher')
 def get_openvpn_cipher(cipher):
     if cipher in openvpn_translate:
         return openvpn_translate[cipher].upper()
     return cipher.upper()
 
 @register_filter('openvpn_data_ciphers')
 def get_openvpn_data_ciphers(ciphers):
     out = []
     for cipher in ciphers:
         if cipher in openvpn_translate:
             out.append(openvpn_translate[cipher])
         else:
             out.append(cipher)
     return ':'.join(out).upper()
 
 @register_filter('snmp_auth_oid')
 def snmp_auth_oid(type):
     if type not in ['md5', 'sha', 'aes', 'des', 'none']:
         raise ValueError()
 
     OIDs = {
         'md5' : '.1.3.6.1.6.3.10.1.1.2',
         'sha' : '.1.3.6.1.6.3.10.1.1.3',
         'aes' : '.1.3.6.1.6.3.10.1.2.4',
         'des' : '.1.3.6.1.6.3.10.1.2.2',
         'none': '.1.3.6.1.6.3.10.1.2.1'
     }
     return OIDs[type]
 
 @register_filter('nft_action')
 def nft_action(vyos_action):
     if vyos_action == 'accept':
         return 'return'
     return vyos_action
 
 @register_filter('nft_rule')
 def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'):
     from vyos.firewall import parse_rule
     return parse_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name)
 
 @register_filter('nft_default_rule')
 def nft_default_rule(fw_conf, fw_name, family):
     output = ['counter']
     default_action = fw_conf['default_action']
     #family = 'ipv6' if ipv6 else 'ipv4'
 
     if 'default_log' in fw_conf:
         action_suffix = default_action[:1].upper()
         output.append(f'log prefix "[{family}-{fw_name[:19]}-default-{action_suffix}]"')
 
     #output.append(nft_action(default_action))
     output.append(f'{default_action}')
     if 'default_jump_target' in fw_conf:
         target = fw_conf['default_jump_target']
         def_suffix = '6' if family == 'ipv6' else ''
         output.append(f'NAME{def_suffix}_{target}')
 
     output.append(f'comment "{fw_name} default-action {default_action}"')
     return " ".join(output)
 
 @register_filter('nft_state_policy')
-def nft_state_policy(conf, state):
+def nft_state_policy(conf, state, bridge=False):
     out = [f'ct state {state}']
 
+    action = conf['action'] if 'action' in conf else None
+
+    if bridge and action == 'reject':
+        action = 'drop' # T7148 - Bridge cannot use reject
+
     if 'log' in conf:
         log_state = state[:3].upper()
-        log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper()
+        log_action = (action if action else 'accept')[:1].upper()
         out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"')
 
         if 'log_level' in conf:
             log_level = conf['log_level']
             out.append(f'level {log_level}')
 
     out.append('counter')
 
-    if 'action' in conf:
-        out.append(conf['action'])
+    if action:
+        out.append(action)
 
     return " ".join(out)
 
 @register_filter('nft_intra_zone_action')
 def nft_intra_zone_action(zone_conf, ipv6=False):
     if 'intra_zone_filtering' in zone_conf:
         intra_zone = zone_conf['intra_zone_filtering']
         fw_name = 'ipv6_name' if ipv6 else 'name'
         name_prefix = 'NAME6_' if ipv6 else 'NAME_'
 
         if 'action' in intra_zone:
             if intra_zone['action'] == 'accept':
                 return 'return'
             return intra_zone['action']
         elif dict_search_args(intra_zone, 'firewall', fw_name):
             name = dict_search_args(intra_zone, 'firewall', fw_name)
             return f'jump {name_prefix}{name}'
     return 'return'
 
 @register_filter('nft_nested_group')
 def nft_nested_group(out_list, includes, groups, key):
     if not vyos_defined(out_list):
         out_list = []
 
     def add_includes(name):
         if key in groups[name]:
             for item in groups[name][key]:
                 if item in out_list:
                     continue
                 out_list.append(item)
 
         if 'include' in groups[name]:
             for name_inc in groups[name]['include']:
                 add_includes(name_inc)
 
     for name in includes:
         add_includes(name)
     return out_list
 
 @register_filter('nat_rule')
 def nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
     from vyos.nat import parse_nat_rule
     return parse_nat_rule(rule_conf, rule_id, nat_type, ipv6)
 
 @register_filter('nat_static_rule')
 def nat_static_rule(rule_conf, rule_id, nat_type):
     from vyos.nat import parse_nat_static_rule
     return parse_nat_static_rule(rule_conf, rule_id, nat_type)
 
 @register_filter('conntrack_rule')
 def conntrack_rule(rule_conf, rule_id, action, ipv6=False):
     ip_prefix = 'ip6' if ipv6 else 'ip'
     def_suffix = '6' if ipv6 else ''
     output = []
 
     if 'inbound_interface' in rule_conf:
         ifname = rule_conf['inbound_interface']
         if ifname != 'any':
             output.append(f'iifname {ifname}')
 
     if 'protocol' in rule_conf:
         if action != 'timeout':
             proto = rule_conf['protocol']
         else:
             for protocol, protocol_config in rule_conf['protocol'].items():
                 proto = protocol
         if proto != 'all':
             output.append(f'meta l4proto {proto}')
 
     tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
     if tcp_flags and action != 'timeout':
         from vyos.firewall import parse_tcp_flags
         output.append(parse_tcp_flags(tcp_flags))
 
     for side in ['source', 'destination']:
         if side in rule_conf:
             side_conf = rule_conf[side]
             prefix = side[0]
 
             if 'address' in side_conf:
                 address = side_conf['address']
                 operator = ''
                 if address[0] == '!':
                     operator = '!='
                     address = address[1:]
                 output.append(f'{ip_prefix} {prefix}addr {operator} {address}')
 
             if 'port' in side_conf:
                 port = side_conf['port']
                 operator = ''
                 if port[0] == '!':
                     operator = '!='
                     port = port[1:]
                 output.append(f'th {prefix}port {operator} {port}')
 
             if 'group' in side_conf:
                 group = side_conf['group']
 
                 if 'address_group' in group:
                     group_name = group['address_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}')
                 # Generate firewall group domain-group
                 elif 'domain_group' in group:
                     group_name = group['domain_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}')
                 elif 'network_group' in group:
                     group_name = group['network_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}')
                 if 'port_group' in group:
                     group_name = group['port_group']
 
                     if proto == 'tcp_udp':
                         proto = 'th'
 
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
 
                     output.append(f'{proto} {prefix}port {operator} @P_{group_name}')
 
     if action == 'ignore':
         output.append('counter notrack')
         output.append(f'comment "ignore-{rule_id}"')
     else:
         output.append(f'counter ct timeout set ct-timeout-{rule_id}')
         output.append(f'comment "timeout-{rule_id}"')
 
     return " ".join(output)
 
 @register_filter('conntrack_ct_policy')
 def conntrack_ct_policy(protocol_conf):
     output = []
     for item in protocol_conf:
         item_value = protocol_conf[item]
         output.append(f'{item}: {item_value}')
 
     return ", ".join(output)
 
 @register_filter('wlb_nft_rule')
 def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
     from vyos.wanloadbalance import nft_rule as wlb_nft_rule
     return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark)
 
 @register_filter('range_to_regex')
 def range_to_regex(num_range):
     """Convert range of numbers or list of ranges
        to regex
 
        % range_to_regex('11-12')
        '(1[1-2])'
        % range_to_regex(['11-12', '14-15'])
        '(1[1-2]|1[4-5])'
     """
     from vyos.range_regex import range_to_regex
     if isinstance(num_range, list):
         data = []
         for entry in num_range:
             if '-' not in entry:
                 data.append(entry)
             else:
                 data.append(range_to_regex(entry))
         return f'({"|".join(data)})'
 
     if '-' not in num_range:
         return num_range
 
     regex = range_to_regex(num_range)
     return f'({regex})'
 
 @register_filter('kea_address_json')
 def kea_address_json(addresses):
     from json import dumps
     from vyos.utils.network import is_addr_assigned
 
     out = []
 
     for address in addresses:
         ifname = is_addr_assigned(address, return_ifname=True, include_vrf=True)
 
         if not ifname:
             continue
 
         out.append(f'{ifname}/{address}')
 
     return dumps(out)
 
 @register_filter('kea_high_availability_json')
 def kea_high_availability_json(config):
     from json import dumps
 
     source_addr = config['source_address']
     remote_addr = config['remote']
     ha_mode = 'hot-standby' if config['mode'] == 'active-passive' else 'load-balancing'
     ha_role = config['status']
 
     if ha_role == 'primary':
         peer1_role = 'primary'
         peer2_role = 'standby' if ha_mode == 'hot-standby' else 'secondary'
     else:
         peer1_role = 'standby' if ha_mode == 'hot-standby' else 'secondary'
         peer2_role = 'primary'
 
     data = {
         'this-server-name': os.uname()[1],
         'mode': ha_mode,
         'heartbeat-delay': 10000,
         'max-response-delay': 10000,
         'max-ack-delay': 5000,
         'max-unacked-clients': 0,
         'peers': [
         {
             'name': os.uname()[1],
             'url': f'http://{source_addr}:647/',
             'role': peer1_role,
             'auto-failover': True
         },
         {
             'name': config['name'],
             'url': f'http://{remote_addr}:647/',
             'role': peer2_role,
             'auto-failover': True
         }]
     }
 
     if 'ca_cert_file' in config:
         data['trust-anchor'] = config['ca_cert_file']
 
     if 'cert_file' in config:
         data['cert-file'] = config['cert_file']
 
     if 'cert_key_file' in config:
         data['key-file'] = config['cert_key_file']
 
     return dumps(data)
 
 @register_filter('kea_shared_network_json')
 def kea_shared_network_json(shared_networks):
     from vyos.kea import kea_parse_options
     from vyos.kea import kea_parse_subnet
     from json import dumps
     out = []
 
     for name, config in shared_networks.items():
         if 'disable' in config:
             continue
 
         network = {
             'name': name,
             'authoritative': ('authoritative' in config),
             'subnet4': []
         }
 
         if 'option' in config:
             network['option-data'] = kea_parse_options(config['option'])
 
             if 'bootfile_name' in config['option']:
                 network['boot-file-name'] = config['option']['bootfile_name']
 
             if 'bootfile_server' in config['option']:
                 network['next-server'] = config['option']['bootfile_server']
 
         if 'subnet' in config:
             for subnet, subnet_config in config['subnet'].items():
                 if 'disable' in subnet_config:
                     continue
                 network['subnet4'].append(kea_parse_subnet(subnet, subnet_config))
 
         out.append(network)
 
     return dumps(out, indent=4)
 
 @register_filter('kea6_shared_network_json')
 def kea6_shared_network_json(shared_networks):
     from vyos.kea import kea6_parse_options
     from vyos.kea import kea6_parse_subnet
     from json import dumps
     out = []
 
     for name, config in shared_networks.items():
         if 'disable' in config:
             continue
 
         network = {
             'name': name,
             'subnet6': []
         }
 
         if 'option' in config:
             network['option-data'] = kea6_parse_options(config['option'])
 
         if 'interface' in config:
             network['interface'] = config['interface']
 
         if 'subnet' in config:
             for subnet, subnet_config in config['subnet'].items():
                 network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config))
 
         out.append(network)
 
     return dumps(out, indent=4)
 
 @register_test('vyos_defined')
 def vyos_defined(value, test_value=None, var_type=None):
     """
     Jinja2 plugin to test if a variable is defined and not none - vyos_defined
     will test value if defined and is not none and return true or false.
 
     If test_value is supplied, the value must also pass == test_value to return true.
     If var_type is supplied, the value must also be of the specified class/type
 
     Examples:
     1. Test if var is defined and not none:
     {% if foo is vyos_defined %}
     ...
     {% endif %}
 
     2. Test if variable is defined, not none and has value "something"
     {% if bar is vyos_defined("something") %}
     ...
     {% endif %}
 
     Parameters
     ----------
     value : any
         Value to test from ansible
     test_value : any, optional
         Value to test in addition of defined and not none, by default None
     var_type : ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'], optional
         Type or Class to test for
 
     Returns
     -------
     boolean
         True if variable matches criteria, False in other cases.
 
     Implementation inspired and re-used from https://github.com/aristanetworks/ansible-avd/
     """
 
     from jinja2 import Undefined
 
     if isinstance(value, Undefined) or value is None:
         # Invalid value - return false
         return False
     elif test_value is not None and value != test_value:
         # Valid value but not matching the optional argument
         return False
     elif str(var_type).lower() in ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'] and str(var_type).lower() != type(value).__name__:
         # Invalid class - return false
         return False
     else:
         # Valid value and is matching optional argument if provided - return true
         return True
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 93d41a7f7..33144c7fa 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -1,1270 +1,1277 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2021-2024 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 unittest
 
 from glob import glob
 from time import sleep
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 from vyos.utils.process import run
 from vyos.utils.file import read_file
 
 sysfs_config = {
     'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'},
     'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'},
     'directed_broadcast': {'sysfs': '/proc/sys/net/ipv4/conf/all/bc_forwarding', 'default': '1', 'test_value': 'disable'},
     'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'},
     'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
     'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'},
     'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'},
     'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
     'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'},
     'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'},
     'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'}
 }
 
 def get_sysctl(parameter):
     tmp = parameter.replace(r'.', r'/')
     return read_file(f'/proc/sys/{tmp}')
 
 class TestFirewall(VyOSUnitTestSHIM.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestFirewall, cls).setUpClass()
 
         # ensure we can also run this test on a live system - so lets clean
         # out the current configuration :)
         cls.cli_delete(cls, ['firewall'])
 
     @classmethod
     def tearDownClass(cls):
         super(TestFirewall, cls).tearDownClass()
 
     def tearDown(self):
         self.cli_delete(['firewall'])
         self.cli_commit()
 
         # Verify chains/sets are cleaned up from nftables
         nftables_search = [
             ['set M_smoketest_mac'],
             ['set N_smoketest_network'],
             ['set P_smoketest_port'],
             ['set D_smoketest_domain'],
             ['set RECENT_smoketest_4'],
             ['chain NAME_smoketest']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True)
 
     def wait_for_domain_resolver(self, table, set_name, element, max_wait=10):
         # Resolver no longer blocks commit, need to wait for daemon to populate set
         count = 0
         while count < max_wait:
             code = run(f'sudo nft get element {table} {set_name} {{ {element} }}')
             if code == 0:
                 return True
             count += 1
             sleep(1)
         return False
 
     def test_geoip(self):
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match'])
 
         self.cli_commit()
 
         nftables_search = [
             ['ip saddr @GEOIP_CC_name_smoketest_1', 'drop'],
             ['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept']
         ]
 
         # -t prevents 1000+ GeoIP elements being returned
         self.verify_nftables(nftables_search, 'ip vyos_filter', args='-t')
 
     def test_groups(self):
         hostmap_path = ['system', 'static-host-mapping', 'host-name']
         example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11']
 
         self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5'])
         for ips in example_org:
             self.cli_set(hostmap_path + ['example.org', 'inet', ips])
 
         self.cli_commit()
 
         self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05'])
         self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
         self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
         self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123'])
         self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com'])
         self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org'])
         self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0'])
         self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest'])
         self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0'])
 
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'address', '172.16.10.10'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface'])
 
         # Create container network so test won't fail
         self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24'])
 
         self.cli_commit()
 
         self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5')
 
         nftables_search = [
             ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'accept'],
             ['elements = { 172.16.99.0/24 }'],
             ['elements = { 53, 123 }'],
             ['ether saddr @M_smoketest_mac', 'accept'],
             ['elements = { 00:01:02:03:04:05 }'],
             ['set D_smoketest_domain'],
             ['elements = { 192.0.2.5, 192.0.2.8,'],
             ['192.0.2.10, 192.0.2.11 }'],
             ['ip saddr @D_smoketest_domain', 'accept'],
             ['oifname != @I_smoketest_interface', 'accept']
         ]
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
         self.cli_delete(['system', 'static-host-mapping'])
         self.cli_commit()
 
     def test_nested_groups(self):
         self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
         self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24'])
         self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network'])
         self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
         self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'port', '123'])
         self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'include', 'smoketest_port'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network1'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port1'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp'])
 
         self.cli_commit()
 
         # Test circular includes
         self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1'])
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_delete(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1'])
 
         nftables_search = [
             ['ip saddr @N_smoketest_network1', 'th dport @P_smoketest_port1', 'accept'],
             ['elements = { 172.16.99.0/24, 172.16.101.0/24 }'],
             ['elements = { 53, 123 }']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
     def test_ipv4_basic_rules(self):
         name = 'smoketest'
         interface = 'eth0'
         interface_inv = '!eth0'
         interface_wc = 'l2tp*'
         mss_range = '501-1460'
         conn_mark = '555'
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'source', 'address', '172.16.20.10'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '172.16.10.10'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log-options', 'level', 'debug'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'ttl', 'eq', '15'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'destination', 'port', '8888'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log-options', 'level', 'err'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'syn'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'not', 'ack'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'ttl', 'gt', '102'])
 
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-log'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'destination', 'port', '22'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'limit', 'rate', '5/minute'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'destination', 'port', '22'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'count', '10'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'time', 'minute'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'packet-type', 'host'])
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'name', interface_wc])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark])
 
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-log'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'name', interface_inv])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark])
 
         self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'rule', '1', 'protocol', 'udp'])
 
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'notrack'])
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'set', 'mark', '55'])
 
         self.cli_commit()
 
         mark_hex = "{0:#010x}".format(int(conn_mark))
 
         nftables_search = [
             ['chain VYOS_FORWARD_filter'],
             ['type filter hook forward priority filter; policy accept;'],
             ['tcp dport 22', 'limit rate 5/minute', 'accept'],
             ['tcp dport 22', 'add @RECENT_FWD_filter_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'meta pkttype host', 'drop'],
             ['log prefix "[ipv4-FWD-filter-default-D]"','FWD-filter default-action drop', 'drop'],
             ['chain VYOS_INPUT_filter'],
             ['type filter hook input priority filter; policy accept;'],
             ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface_wc}"', 'meta pkttype broadcast', 'accept'],
             ['meta l4proto gre', f'ct mark {mark_hex}', 'return'],
             ['INP-filter default-action accept', 'accept'],
             ['chain VYOS_OUTPUT_filter'],
             ['type filter hook output priority filter; policy accept;'],
             ['meta l4proto gre', f'oifname != "{interface}"', 'drop'],
             ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'],
             ['log prefix "[ipv4-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
             ['chain VYOS_OUTPUT_raw'],
             ['type filter hook output priority raw; policy accept;'],
             ['udp', 'accept'],
             ['OUT-raw default-action drop', 'drop'],
             ['chain VYOS_PREROUTING_raw'],
             ['type filter hook prerouting priority raw; policy accept;'],
             ['tcp dport 23', 'meta mark set 0x00000037', 'notrack'],
             ['PRE-raw default-action accept', 'accept'],
             ['chain NAME_smoketest'],
             ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'],
             ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'],
             ['log prefix "[ipv4-NAM-smoketest-default-D]"','smoketest default-action', 'drop']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
     def test_ipv4_advanced(self):
         name = 'smoketest-adv'
         name2 = 'smoketest-adv2'
         interface = 'eth0'
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '64'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '512'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '1024'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '17'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '52'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'group', '66'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'snapshot-length', '6666'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'queue-threshold','32000'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length', '1-30000'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length-exclude', '60000-65535'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp', '3-11'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp-exclude', '21-25'])
 
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'address', '198.51.100.1-198.51.100.50'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'mark', '1010'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'set', 'dscp', '32'])
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'mark', '!98765'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'action', 'queue'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'queue', '3'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'protocol', 'udp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'action', 'queue'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'fanout'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'bypass'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue', '0-15'])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain VYOS_FORWARD_filter'],
             ['type filter hook forward priority filter; policy accept;'],
             ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', 'ip dscp set cs4', f'jump NAME_{name}'],
             ['FWD-filter default-action drop', 'drop'],
             ['chain VYOS_INPUT_filter'],
             ['type filter hook input priority filter; policy accept;'],
             ['meta mark != 0x000181cd', 'meta l4proto tcp','queue to 3'],
             ['meta l4proto udp','queue flags bypass,fanout to 0-15'],
             ['INP-filter default-action accept', 'accept'],
             [f'chain NAME_{name}'],
             ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'],
             ['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'],
             [f'log prefix "[ipv4-NAM-{name}-default-D]"', 'drop']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
     def test_ipv4_synproxy(self):
         tcp_mss = '1460'
         tcp_wscale = '7'
         dport = '22'
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', dport])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'synproxy', 'tcp', 'mss', tcp_mss])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'synproxy', 'tcp', 'window-scale', tcp_wscale])
 
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'synproxy'])
 
         self.cli_commit()
 
         nftables_search = [
             [f'tcp dport {dport} ct state invalid,untracked', f'synproxy mss {tcp_mss} wscale {tcp_wscale} timestamp sack-perm']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
 
     def test_ipv4_mask(self):
         name = 'smoketest-mask'
         interface = 'eth0'
 
         self.cli_set(['firewall', 'group', 'address-group', 'mask_group', 'address', '1.1.1.1'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '0.0.1.2'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address-mask', '0.0.255.255'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address', '!0.0.3.4'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address-mask', '0.0.255.255'])
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'address-mask', '0.0.255.255'])
 
         self.cli_commit()
 
         nftables_search = [
             [f'daddr & 0.0.255.255 == 0.0.1.2'],
             [f'saddr & 0.0.255.255 != 0.0.3.4'],
             [f'saddr & 0.0.255.255 == @A_mask_group']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
     def test_ipv4_dynamic_groups(self):
         group01 = 'knock01'
         group02 = 'allowed'
 
         self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group01])
         self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group02])
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
 
         self.cli_commit()
 
         nftables_search = [
             [f'DA_{group01}'],
             [f'DA_{group02}'],
             ['type ipv4_addr'],
             ['flags dynamic,timeout'],
             ['chain VYOS_INPUT_filter {'],
             ['type filter hook input priority filter', 'policy accept'],
             ['tcp dport 5151', f'update @DA_{group01}', '{ ip saddr timeout 30s }', 'drop'],
             ['tcp dport 7272', f'ip saddr @DA_{group01}', f'update @DA_{group02}', '{ ip saddr timeout 5m }', 'drop'],
             ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
     def test_ipv6_basic_rules(self):
         name = 'v6-smoketest'
         interface = 'eth0'
 
         self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'source', 'address', '2002::1-2002::10'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '2002::1:1'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log-options', 'level', 'crit'])
 
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-log'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'protocol', 'tcp_udp'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'destination', 'port', '8888'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'inbound-interface', 'name', interface])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'udp'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'source', 'address', '2002::1:2'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'name', interface])
 
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-log'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'action', 'return'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'outbound-interface', 'name', interface])
 
         self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'rule', '1', 'action', 'notrack'])
         self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'rule', '1', 'protocol', 'udp'])
 
         self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
         self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'set', 'hop-limit', '79'])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain VYOS_IPV6_FORWARD_filter'],
             ['type filter hook forward priority filter; policy accept;'],
             ['meta l4proto { tcp, udp }', 'th dport 8888', f'iifname "{interface}"', 'reject'],
             ['log prefix "[ipv6-FWD-filter-default-A]"','FWD-filter default-action accept', 'accept'],
             ['chain VYOS_IPV6_INPUT_filter'],
             ['type filter hook input priority filter; policy accept;'],
             ['meta l4proto udp', 'ip6 saddr 2002::1:2', f'iifname "{interface}"', 'accept'],
             ['INP-filter default-action accept', 'accept'],
             ['chain VYOS_IPV6_OUTPUT_filter'],
             ['type filter hook output priority filter; policy accept;'],
             ['meta l4proto gre', f'oifname "{interface}"', 'return'],
             ['log prefix "[ipv6-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
             ['chain VYOS_IPV6_OUTPUT_raw'],
             ['type filter hook output priority raw; policy accept;'],
             ['udp', 'notrack'],
             ['OUT-raw default-action drop', 'drop'],
             ['chain VYOS_IPV6_PREROUTING_raw'],
             ['type filter hook prerouting priority raw; policy accept;'],
             ['tcp dport 23', 'ip6 hoplimit set 79', 'drop'],
             ['PRE-raw default-action accept', 'accept'],
             [f'chain NAME6_{name}'],
             ['saddr 2002::1-2002::10', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'],
             [f'"NAM-{name} default-action drop"', f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop'],
             ['jump VYOS_STATE_POLICY6'],
             ['chain VYOS_STATE_POLICY6'],
             ['ct state established', 'accept'],
             ['ct state invalid', 'drop'],
             ['ct state related', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
     def test_ipv6_advanced(self):
         name = 'v6-smoke-adv'
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '65'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '513'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '1025'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '18'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '53'])
 
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length', '1-1999'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length-exclude', '60000-65535'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp', '4-14'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp-exclude', '31-35'])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'default-action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'source', 'address', '2001:db8::/64'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'mark', '!6655-7766'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'jump-target', name])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain VYOS_IPV6_FORWARD_filter'],
             ['type filter hook forward priority filter; policy accept;'],
             ['ip6 length 1-1999', 'ip6 length != 60000-65535', 'ip6 dscp 0x04-0x0e', 'ip6 dscp != 0x1f-0x23', 'accept'],
             ['chain VYOS_IPV6_INPUT_filter'],
             ['type filter hook input priority filter; policy accept;'],
             ['ip6 saddr 2001:db8::/64', 'meta mark != 0x000019ff-0x00001e56', f'jump NAME6_{name}'],
             [f'chain NAME6_{name}'],
             ['ip6 length { 65, 513, 1025 }', 'ip6 dscp { af21, 0x35 }', 'accept'],
             [f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop']
         ]
 
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
     def test_ipv6_mask(self):
         name = 'v6-smoketest-mask'
         interface = 'eth0'
 
         self.cli_set(['firewall', 'group', 'ipv6-address-group', 'mask_group', 'address', '::beef'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff'])
 
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group'])
         self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff'])
 
         self.cli_commit()
 
         nftables_search = [
             ['daddr & ::ffff:ffff:ffff:ffff == ::1111:2222:3333:4444'],
             ['saddr & ::ffff:ffff:ffff:ffff != ::aaaa:bbbb:cccc:dddd'],
             ['saddr & ::ffff:ffff:ffff:ffff == @A6_mask_group']
         ]
 
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
     def test_ipv6_dynamic_groups(self):
         group01 = 'knock01'
         group02 = 'allowed'
 
         self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group01])
         self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group02])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
 
         self.cli_commit()
 
         nftables_search = [
             [f'DA6_{group01}'],
             [f'DA6_{group02}'],
             ['type ipv6_addr'],
             ['flags dynamic,timeout'],
             ['chain VYOS_IPV6_INPUT_filter {'],
             ['type filter hook input priority filter', 'policy accept'],
             ['tcp dport 5151', f'update @DA6_{group01}', '{ ip6 saddr timeout 30s }', 'drop'],
             ['tcp dport 7272', f'ip6 saddr @DA6_{group01}', f'update @DA6_{group02}', '{ ip6 saddr timeout 5m }', 'drop'],
             ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
     def test_ipv4_global_state(self):
         self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
 
         self.cli_commit()
 
         nftables_search = [
             ['jump VYOS_STATE_POLICY'],
             ['chain VYOS_STATE_POLICY'],
             ['ct state established', 'accept'],
             ['ct state invalid', 'drop'],
             ['ct state related', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
+        # T7148 - Ensure bridge rule reject -> drop
+        self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject'])
+        self.cli_commit()
+
+        self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter')
+        self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter')
+
         # Check conntrack is enabled from state-policy
         self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
         self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
 
     def test_ipv4_state_and_status_rules(self):
         name = 'smoketest-state'
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'connection-status', 'nat', 'destination'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'connection-status', 'nat', 'source'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'state', 'related'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'ftp'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'pptp'])
 
         self.cli_commit()
 
         nftables_search = [
             ['ct state { established, related }', 'accept'],
             ['ct state invalid', 'reject'],
             ['ct state new', 'ct status dnat', 'accept'],
             ['ct state { established, new }', 'ct status snat', 'accept'],
             ['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'],
             ['drop', f'comment "NAM-{name} default-action drop"']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
         # Check conntrack
         self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
         self.verify_nftables_chain([['return']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
 
     def test_bridge_firewall(self):
         name = 'smoketest'
         interface_in = 'eth0'
         mac_address = '00:53:00:00:00:01'
         vlan_id = '12'
         vlan_prior = '3'
 
         # Check bridge-nf-call-iptables default value: 0
         self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '0')
         self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
 
         self.cli_set(['firewall', 'group', 'ipv6-address-group', 'AGV6', 'address', '2001:db1::1'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'ipv4'])
         self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'invalid-connections'])
 
         self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept'])
         self.cli_set(['firewall', 'bridge', 'name', name, 'default-log'])
         self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'source', 'mac-address', mac_address])
         self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'name', interface_in])
         self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log'])
         self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log-options', 'level', 'crit'])
 
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-action', 'drop'])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-log'])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'ethernet-type', 'ipv4'])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'set', 'connection-mark', '123123'])
 
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump'])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior])
         self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'set', 'ttl', '128'])
 
         self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'inbound-interface', 'name', interface_in])
         self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'source', 'address', '192.0.2.2'])
         self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'state', 'new'])
 
         self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'action', 'notrack'])
         self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'destination', 'group', 'ipv6-address-group', 'AGV6'])
         self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '2', 'ethernet-type', 'arp'])
         self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '2', 'action', 'accept'])
 
 
         self.cli_commit()
 
         nftables_search = [
             ['set A6_AGV6'],
             ['type ipv6_addr'],
             ['elements', '2001:db1::1'],
             ['chain VYOS_FORWARD_filter'],
             ['type filter hook forward priority filter; policy accept;'],
             ['jump VYOS_STATE_POLICY'],
             [f'vlan id {vlan_id}', 'vlan type ip', 'ct mark set 0x0001e0f3', 'accept'],
             [f'vlan pcp {vlan_prior}', 'ip ttl set 128', f'jump NAME_{name}'],
             ['log prefix "[bri-FWD-filter-default-D]"', 'drop', 'FWD-filter default-action drop'],
             [f'chain NAME_{name}'],
             [f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'],
             ['accept', f'{name} default-action accept'],
             ['chain VYOS_INPUT_filter'],
             ['type filter hook input priority filter; policy accept;'],
             ['ct state new', 'ip saddr 192.0.2.2', f'iifname "{interface_in}"', 'accept'],
             ['chain VYOS_OUTPUT_filter'],
             ['type filter hook output priority filter; policy accept;'],
             ['ct state invalid', 'udp sport 67', 'udp dport 68', 'accept'],
             ['ct state invalid', 'ether type arp', 'accept'],
             ['ct state invalid', 'ether type 0x8864', 'accept'],
             ['chain VYOS_PREROUTING_filter'],
             ['type filter hook prerouting priority filter; policy accept;'],
             ['ip6 daddr @A6_AGV6', 'notrack'],
             ['ether type arp', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'bridge vyos_filter')
         ## Check bridge-nf-call-iptables is set to 1, and for ipv6 remains on default 0
         self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '1')
         self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
 
     def test_source_validation(self):
         # Strict
         self.cli_set(['firewall', 'global-options', 'source-validation', 'strict'])
         self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'strict'])
         self.cli_commit()
 
         nftables_strict_search = [
             ['fib saddr . iif oif 0', 'drop']
         ]
 
         self.verify_nftables_chain(nftables_strict_search, 'ip raw', 'vyos_global_rpfilter')
         self.verify_nftables_chain(nftables_strict_search, 'ip6 raw', 'vyos_global_rpfilter')
 
         # Loose
         self.cli_set(['firewall', 'global-options', 'source-validation', 'loose'])
         self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'loose'])
         self.cli_commit()
 
         nftables_loose_search = [
             ['fib saddr oif 0', 'drop']
         ]
 
         self.verify_nftables_chain(nftables_loose_search, 'ip raw', 'vyos_global_rpfilter')
         self.verify_nftables_chain(nftables_loose_search, 'ip6 raw', 'vyos_global_rpfilter')
 
     def test_sysfs(self):
         for name, conf in sysfs_config.items():
             paths = glob(conf['sysfs'])
             for path in paths:
                 with open(path, 'r') as f:
                     self.assertEqual(f.read().strip(), conf['default'], msg=path)
 
             self.cli_set(['firewall', 'global-options', name.replace("_", "-"), conf['test_value']])
 
         self.cli_commit()
 
         for name, conf in sysfs_config.items():
             paths = glob(conf['sysfs'])
             for path in paths:
                 with open(path, 'r') as f:
                     self.assertNotEqual(f.read().strip(), conf['default'], msg=path)
 
     def test_timeout_sysctl(self):
         timeout_config = {
             'net.netfilter.nf_conntrack_icmp_timeout' :{
                 'cli'           : ['global-options', 'timeout', 'icmp'],
                 'test_value'    : '180',
                 'default_value' : '30',
             },
             'net.netfilter.nf_conntrack_generic_timeout' :{
                 'cli'           : ['global-options', 'timeout', 'other'],
                 'test_value'    : '1200',
                 'default_value' : '600',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_close_wait' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'close-wait'],
                 'test_value'    : '30',
                 'default_value' : '60',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_close' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'close'],
                 'test_value'    : '20',
                 'default_value' : '10',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_established' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'established'],
                 'test_value'    : '1000',
                 'default_value' : '432000',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_fin_wait' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'fin-wait'],
                 'test_value'    : '240',
                 'default_value' : '120',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_last_ack' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'last-ack'],
                 'test_value'    : '300',
                 'default_value' : '30',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_syn_recv' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'syn-recv'],
                 'test_value'    : '100',
                 'default_value' : '60',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_syn_sent' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'syn-sent'],
                 'test_value'    : '300',
                 'default_value' : '120',
             },
             'net.netfilter.nf_conntrack_tcp_timeout_time_wait' :{
                 'cli'           : ['global-options', 'timeout', 'tcp', 'time-wait'],
                 'test_value'    : '303',
                 'default_value' : '120',
             },
             'net.netfilter.nf_conntrack_udp_timeout' :{
                 'cli'           : ['global-options', 'timeout', 'udp', 'other'],
                 'test_value'    : '90',
                 'default_value' : '30',
             },
             'net.netfilter.nf_conntrack_udp_timeout_stream' :{
                 'cli'           : ['global-options', 'timeout', 'udp', 'stream'],
                 'test_value'    : '200',
                 'default_value' : '180',
             },
         }
 
         for parameter, parameter_config in timeout_config.items():
             self.cli_set(['firewall'] + parameter_config['cli'] + [parameter_config['test_value']])
 
         # commit changes
         self.cli_commit()
 
         # validate configuration
         for parameter, parameter_config in timeout_config.items():
             tmp = parameter_config['test_value']
             self.assertEqual(get_sysctl(f'{parameter}'), tmp)
 
         # delete all configuration options and revert back to defaults
         self.cli_delete(['firewall', 'global-options', 'timeout'])
         self.cli_commit()
 
         # validate configuration
         for parameter, parameter_config in timeout_config.items():
             self.assertEqual(get_sysctl(f'{parameter}'), parameter_config['default_value'])
 
 ### Zone
     def test_zone_basic(self):
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop'])
         self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0'])
         self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
         self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6'])
         self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone'])
         self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'log'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
         self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain VYOS_ZONE_FORWARD'],
             ['type filter hook forward priority filter + 1'],
             ['chain VYOS_ZONE_OUTPUT'],
             ['type filter hook output priority filter + 1'],
             ['chain VYOS_ZONE_LOCAL'],
             ['type filter hook input priority filter + 1'],
             ['chain VZONE_smoketest-eth0'],
             ['chain VZONE_smoketest-local_IN'],
             ['chain VZONE_smoketest-local_OUT'],
             ['oifname "eth0"', 'jump VZONE_smoketest-eth0'],
             ['jump VZONE_smoketest-local_IN'],
             ['jump VZONE_smoketest-local_OUT'],
             ['iifname "eth0"', 'jump NAME_smoketest'],
             ['oifname "eth0"', 'jump NAME_smoketest'],
             ['jump VYOS_STATE_POLICY'],
             ['chain VYOS_STATE_POLICY'],
             ['ct state established', 'log prefix "[STATE-POLICY-EST-A]"', 'accept'],
             ['ct state invalid', 'drop'],
             ['ct state related', 'accept']
         ]
 
         nftables_search_v6 = [
             ['chain VYOS_ZONE_FORWARD'],
             ['type filter hook forward priority filter + 1'],
             ['chain VYOS_ZONE_OUTPUT'],
             ['type filter hook output priority filter + 1'],
             ['chain VYOS_ZONE_LOCAL'],
             ['type filter hook input priority filter + 1'],
             ['chain VZONE_smoketest-eth0'],
             ['chain VZONE_smoketest-local_IN'],
             ['chain VZONE_smoketest-local_OUT'],
             ['oifname "eth0"', 'jump VZONE_smoketest-eth0'],
             ['jump VZONE_smoketest-local_IN'],
             ['jump VZONE_smoketest-local_OUT'],
             ['iifname "eth0"', 'jump NAME6_smoketestv6'],
             ['jump VYOS_STATE_POLICY6'],
             ['chain VYOS_STATE_POLICY6'],
             ['ct state established', 'log prefix "[STATE-POLICY-EST-A]"', 'accept'],
             ['ct state invalid', 'drop'],
             ['ct state related', 'accept']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
         self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
 
     def test_zone_with_vrf(self):
         self.cli_set(['firewall', 'ipv4', 'name', 'ZONE1-to-LOCAL', 'default-action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', 'ZONE2_to_ZONE1', 'default-action', 'continue'])
         self.cli_set(['firewall', 'ipv6', 'name', 'LOCAL_to_ZONE2_v6', 'default-action', 'drop'])
         self.cli_set(['firewall', 'zone', 'LOCAL', 'from', 'ZONE1', 'firewall', 'name', 'ZONE1-to-LOCAL'])
         self.cli_set(['firewall', 'zone', 'LOCAL', 'local-zone'])
         self.cli_set(['firewall', 'zone', 'ZONE1', 'from', 'ZONE2', 'firewall', 'name', 'ZONE2_to_ZONE1'])
         self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth1'])
         self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth2'])
         self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'vrf', 'VRF-1'])
         self.cli_set(['firewall', 'zone', 'ZONE2', 'from', 'LOCAL', 'firewall', 'ipv6-name', 'LOCAL_to_ZONE2_v6'])
         self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'interface', 'vtun66'])
         self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'vrf', 'VRF-2'])
 
         self.cli_set(['vrf', 'name', 'VRF-1', 'table', '101'])
         self.cli_set(['vrf', 'name', 'VRF-2', 'table', '102'])
         self.cli_set(['interfaces', 'ethernet', 'eth0', 'vrf', 'VRF-1'])
         self.cli_set(['interfaces', 'vti', 'vti1', 'vrf', 'VRF-2'])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain NAME_ZONE1-to-LOCAL'],
             ['counter', 'accept', 'comment "NAM-ZONE1-to-LOCAL default-action accept"'],
             ['chain NAME_ZONE2_to_ZONE1'],
             ['counter', 'continue', 'comment "NAM-ZONE2_to_ZONE1 default-action continue"'],
             ['chain VYOS_ZONE_FORWARD'],
             ['type filter hook forward priority filter + 1'],
             ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'],
             ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'],
             ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'],
             ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'],
             ['chain VYOS_ZONE_LOCAL'],
             ['type filter hook input priority filter + 1'],
             ['counter packets', 'jump VZONE_LOCAL_IN'],
             ['chain VYOS_ZONE_OUTPUT'],
             ['type filter hook output priority filter + 1'],
             ['counter packets', 'jump VZONE_LOCAL_OUT'],
             ['chain VZONE_LOCAL_IN'],
             ['iifname { "eth1", "eth2" }', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'],
             ['iifname "VRF-1"', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'],
             ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
             ['chain VZONE_LOCAL_OUT'],
             ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
             ['chain VZONE_ZONE1'],
             ['iifname { "eth1", "eth2" }', 'counter packets', 'return'],
             ['iifname "VRF-1"', 'counter packets', 'return'],
             ['iifname "vtun66"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'],
             ['iifname "vtun66"', 'counter packets', 'return'],
             ['iifname "VRF-2"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'],
             ['iifname "VRF-2"', 'counter packets', 'return'],
             ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'],
             ['chain VZONE_ZONE2'],
             ['iifname "vtun66"', 'counter packets', 'return'],
             ['iifname "VRF-2"', 'counter packets', 'return'],
             ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"']
         ]
 
         nftables_search_v6 = [
             ['chain NAME6_LOCAL_to_ZONE2_v6'],
             ['counter', 'drop', 'comment "NAM-LOCAL_to_ZONE2_v6 default-action drop"'],
             ['chain VYOS_ZONE_FORWARD'],
             ['type filter hook forward priority filter + 1'],
             ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'],
             ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'],
             ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'],
             ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'],
             ['chain VYOS_ZONE_LOCAL'],
             ['type filter hook input priority filter + 1'],
             ['counter packets', 'jump VZONE_LOCAL_IN'],
             ['chain VYOS_ZONE_OUTPUT'],
             ['type filter hook output priority filter + 1'],
             ['counter packets', 'jump VZONE_LOCAL_OUT'],
             ['chain VZONE_LOCAL_IN'],
             ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
             ['chain VZONE_LOCAL_OUT'],
             ['oifname "vtun66"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'],
             ['oifname "vti1"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'],
             ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'],
             ['chain VZONE_ZONE1'],
             ['iifname { "eth1", "eth2" }', 'counter packets', 'return'],
             ['iifname "VRF-1"', 'counter packets', 'return'],
             ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'],
             ['chain VZONE_ZONE2'],
             ['iifname "vtun66"', 'counter packets', 'return'],
             ['iifname "VRF-2"', 'counter packets', 'return'],
             ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
         self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
 
     def test_flow_offload(self):
         self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10'])
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10'])
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware'])
 
         # QEMU virtual NIC does not support hw-tc-offload
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software'])
 
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'offload'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'established'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'related'])
 
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'action', 'offload'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'established'])
         self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'related'])
 
         self.cli_commit()
 
         nftables_search = [
             ['flowtable VYOS_FLOWTABLE_smoketest'],
             ['hook ingress priority filter'],
             ['devices = { eth0.10 }'],
             ['ct state { established, related }', 'meta l4proto { tcp, udp }', 'flow add @VYOS_FLOWTABLE_smoketest'],
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
         # Check conntrack
         self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
         self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
 
     def test_zone_flow_offload(self):
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0'])
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware'])
 
         # QEMU virtual NIC does not support hw-tc-offload
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software'])
 
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'offload'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'offload-target', 'smoketest'])
 
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest', 'rule', '1', 'action', 'offload'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest', 'rule', '1', 'offload-target', 'smoketest'])
 
         self.cli_commit()
 
         nftables_search = [
             ['chain NAME_smoketest'],
             ['flow add @VYOS_FLOWTABLE_smoketest']
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
 
         nftables_search = [
             ['chain NAME6_smoketest'],
             ['flow add @VYOS_FLOWTABLE_smoketest']
         ]
 
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
         # Check conntrack
         self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
         self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
 
     def test_ipsec_metadata_match(self):
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'ipsec', 'match-ipsec-in'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'ipsec', 'match-none-in'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'action', 'continue'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'ipsec', 'match-ipsec-out'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'ipsec', 'match-none-out'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'ipsec', 'match-ipsec-in'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'ipsec', 'match-none-in'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'action', 'continue'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'ipsec', 'match-ipsec-out'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'ipsec', 'match-none-out'])
 
         self.cli_commit()
 
         nftables_search = [
             ['meta ipsec exists', 'accept comment'],
             ['meta ipsec missing', 'drop comment'],
             ['rt ipsec exists', 'continue comment'],
             ['rt ipsec missing', 'reject comment'],
         ]
 
         self.verify_nftables(nftables_search, 'ip vyos_filter')
         self.verify_nftables(nftables_search, 'ip6 vyos_filter')
 
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
 
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
 
         # All valid directional usage of ipsec matches
         self.cli_commit()
 
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
 
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in-indirect'])
 
         # nft does not support ANY usage of 'meta ipsec' under an output hook, it will fail to load cfg
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
     def test_cyclic_jump_validation(self):
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'jump-target', 'smoketest-cycle-2'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'jump-target', 'smoketest-cycle-3'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'log'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
 
         # Multi-level jumps are unwise but allowed
         self.cli_commit()
 
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump'])
         self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
 
         # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
         # It should be caught by conf validation.
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
     def test_gre_match(self):
         name = 'smoketest-gre'
 
         self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'return'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'flags', 'key'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'flags', 'checksum', 'unset'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'key', '1234'])
         self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log'])
 
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'continue'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'gre', 'inner-proto', '0x6558'])
         self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'log'])
 
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'drop'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'gre', 'flags', 'checksum'])
         self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'gre', 'key', '4321'])
 
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'action', 'reject'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'protocol', 'gre'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'version', 'pptp'])
 
         self.cli_commit()
 
         nftables_search_v4 = [
             ['gre protocol 0x6558', 'continue comment'],
             ['gre flags & 5 == 4 @th,32,32 0x4d2', 'accept comment'],
         ]
 
         nftables_search_v6 = [
             ['gre flags & 5 == 5 @th,64,32 0x10e1', 'drop comment'],
             ['gre version 1', 'reject comment'],
         ]
 
         self.verify_nftables(nftables_search_v4, 'ip vyos_filter')
         self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
 
         # GRE match will only work with protocol GRE
         self.cli_delete(['firewall', 'ipv4', 'name', name, 'rule', '1', 'protocol', 'gre'])
 
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_discard()
 
         # GREv1 (PPTP) does not include a key field, match not available
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'flags', 'checksum', 'unset'])
         self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'key', '1234'])
 
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)