diff options
| author | Nicolas Fort <nicolasfort1988@gmail.com> | 2022-10-28 18:19:47 +0000 | 
|---|---|---|
| committer | Nicolas Fort <nicolasfort1988@gmail.com> | 2022-11-19 14:31:32 +0000 | 
| commit | 9a5dfb4b7ec9e065a73511a38e1713aec03eee0e (patch) | |
| tree | 6c27d3413c22f14af358fd28994a243b9fcf5633 | |
| parent | a61e1a78fe116bb44fe55be3493de7c4dbe8db97 (diff) | |
| download | vyos-1x-9a5dfb4b7ec9e065a73511a38e1713aec03eee0e.tar.gz vyos-1x-9a5dfb4b7ec9e065a73511a38e1713aec03eee0e.zip | |
T4780: Firewall: add firewall groups in firewall. Extend matching criteria so this new group can be used in inbound and outbound matcher
| -rw-r--r-- | data/templates/firewall/nftables-defines.j2 | 13 | ||||
| -rw-r--r-- | interface-definitions/firewall.xml.in | 29 | ||||
| -rw-r--r-- | interface-definitions/include/firewall/common-rule.xml.i | 26 | ||||
| -rw-r--r-- | interface-definitions/include/firewall/match-interface.xml.i | 18 | ||||
| -rw-r--r-- | interface-definitions/include/version/firewall-version.xml.i | 2 | ||||
| -rw-r--r-- | python/vyos/firewall.py | 16 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 15 | ||||
| -rwxr-xr-x | src/conf_mode/firewall.py | 3 | ||||
| -rwxr-xr-x | src/conf_mode/policy-route.py | 109 | ||||
| -rwxr-xr-x | src/migration-scripts/firewall/8-to-9 | 91 | 
10 files changed, 194 insertions, 128 deletions
| diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index 5336f7ee6..b9ebf4d5c 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -77,5 +77,18 @@      }  {%         endfor %}  {%     endif %} +{%     if group.interface_group is vyos_defined %} +{%         for group_name, group_conf in group.interface_group.items() %} +{%             set includes = group_conf.include if group_conf.include is vyos_defined else [] %} +    set I_{{ group_name }} { +        type ifname +        flags interval +        auto-merge +{%             if group_conf.interface is vyos_defined or includes %} +        elements = { {{ group_conf.interface | nft_nested_group(includes, group.interface_group, 'interface') | join(",") }} } +{%             endif %} +    } +{%         endfor %} +{%     endif %}  {% endif %}  {% endmacro %} diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 673461036..12584276c 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -134,6 +134,35 @@                #include <include/generic-description.xml.i>              </children>            </tagNode> +          <tagNode name="interface-group"> +            <properties> +              <help>Firewall interface-group</help> +              <constraint> +                <regex>[a-zA-Z0-9][\w\-\.]*</regex> +              </constraint> +            </properties> +            <children> +              <leafNode name="interface"> +                <properties> +                  <help>Interface-group member</help> +                  <completionHelp> +                    <script>${vyos_completion_dir}/list_interfaces.py</script> +                  </completionHelp> +                  <multi/> +                </properties> +              </leafNode> +              <leafNode name="include"> +                <properties> +                  <help>Include another interface-group</help> +                  <completionHelp> +                    <path>firewall group interface-group</path> +                  </completionHelp> +                  <multi/> +                </properties> +              </leafNode> +              #include <include/generic-description.xml.i> +            </children> +          </tagNode>            <tagNode name="ipv6-address-group">              <properties>                <help>Firewall ipv6-address-group</help> diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i index a4f66f5cb..297e6fc1a 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -26,14 +26,22 @@      </leafNode>    </children>  </node> -<leafNode name="inbound-interface"> +<node name="inbound-interface">    <properties>      <help>Match inbound-interface</help> -    <completionHelp> -      <script>${vyos_completion_dir}/list_interfaces.py</script> -    </completionHelp>    </properties> -</leafNode> +  <children> +    #include <include/firewall/match-interface.xml.i> +  </children> +</node> +<node name="outbound-interface"> +  <properties> +    <help>Match outbound-interface</help> +  </properties> +  <children> +    #include <include/firewall/match-interface.xml.i> +  </children> +</node>  <node name="ipsec">    <properties>      <help>Inbound IPsec packets</help> @@ -130,14 +138,6 @@      </leafNode>    </children>  </node> -<leafNode name="outbound-interface"> -  <properties> -    <help>Match outbound-interface</help> -    <completionHelp> -      <script>${vyos_completion_dir}/list_interfaces.py</script> -    </completionHelp> -  </properties> -</leafNode>  <leafNode name="protocol">    <properties>      <help>Protocol to match (protocol name, number, or "all")</help> diff --git a/interface-definitions/include/firewall/match-interface.xml.i b/interface-definitions/include/firewall/match-interface.xml.i new file mode 100644 index 000000000..675a87574 --- /dev/null +++ b/interface-definitions/include/firewall/match-interface.xml.i @@ -0,0 +1,18 @@ +<!-- include start from firewall/match-interface.xml.i --> +<leafNode name="interface-name"> +  <properties> +    <help>Match interface</help> +    <completionHelp> +      <script>${vyos_completion_dir}/list_interfaces.py</script> +    </completionHelp> +  </properties> +</leafNode> +<leafNode name="interface-group"> +  <properties> +    <help>Match interface-group</help> +    <completionHelp> +      <path>firewall group interface-group</path> +    </completionHelp> +  </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i index 065925319..bc04f8d51 100644 --- a/interface-definitions/include/version/firewall-version.xml.i +++ b/interface-definitions/include/version/firewall-version.xml.i @@ -1,3 +1,3 @@  <!-- include start from include/version/firewall-version.xml.i --> -<syntaxVersion component='firewall' version='8'></syntaxVersion> +<syntaxVersion component='firewall' version='9'></syntaxVersion>  <!-- include end --> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 4075e55b0..0e92da8ab 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -249,12 +249,20 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):                  output.append(f'ip6 hoplimit {operator} {value}')      if 'inbound_interface' in rule_conf: -        iiface = rule_conf['inbound_interface'] -        output.append(f'iifname {iiface}') +        if 'interface_name' in rule_conf['inbound_interface']: +            iiface = rule_conf['inbound_interface']['interface_name'] +            output.append(f'iifname {{{iiface}}}') +        else: +            iiface = rule_conf['inbound_interface']['interface_group'] +            output.append(f'iifname @I_{iiface}')      if 'outbound_interface' in rule_conf: -        oiface = rule_conf['outbound_interface'] -        output.append(f'oifname {oiface}') +        if 'interface_name' in rule_conf['outbound_interface']: +            oiface = rule_conf['outbound_interface']['interface_name'] +            output.append(f'oifname {{{oiface}}}') +        else: +            oiface = rule_conf['outbound_interface']['interface_group'] +            output.append(f'oifname @I_{oiface}')      if 'ttl' in rule_conf:          operators = {'eq': '==', 'gt': '>', 'lt': '<'} diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 821925bcd..6af574bdb 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -111,6 +111,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          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', 'vtun0'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) @@ -121,6 +123,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'action', 'accept']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'outbound-interface', 'interface-group', 'smoketest_interface'])          self.cli_set(['firewall', 'interface', 'eth0', 'in', 'name', 'smoketest']) @@ -135,7 +139,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):              ['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', 'return'] +            ['ip saddr @D_smoketest_domain', 'return'], +            ['oifname @I_smoketest_interface', 'return']          ]          self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -209,10 +214,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'name', name, 'rule', '5', 'protocol', 'tcp'])          self.cli_set(['firewall', 'name', name, 'rule', '5', 'tcp', 'flags', 'syn'])          self.cli_set(['firewall', 'name', name, 'rule', '5', 'tcp', 'mss', mss_range]) -        self.cli_set(['firewall', 'name', name, 'rule', '5', 'inbound-interface', interface]) +        self.cli_set(['firewall', 'name', name, 'rule', '5', 'inbound-interface', 'interface-name', interface])          self.cli_set(['firewall', 'name', name, 'rule', '6', 'action', 'return'])          self.cli_set(['firewall', 'name', name, 'rule', '6', 'protocol', 'gre']) -        self.cli_set(['firewall', 'name', name, 'rule', '6', 'outbound-interface', interface]) +        self.cli_set(['firewall', 'name', name, 'rule', '6', 'outbound-interface', 'interface-name', interface])          self.cli_set(['firewall', 'interface', interface, 'in', 'name', name]) @@ -290,11 +295,11 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'action', 'reject'])          self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'protocol', 'tcp_udp'])          self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'destination', 'port', '8888']) -        self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'inbound-interface', interface]) +        self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'inbound-interface', 'interface-name', interface])          self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'action', 'return'])          self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'protocol', 'gre']) -        self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'outbound-interface', interface]) +        self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'outbound-interface', 'interface-name', interface])          self.cli_set(['firewall', 'interface', interface, 'in', 'ipv6-name', name]) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index cbd9cbe90..dcdbf8fab 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -67,7 +67,8 @@ valid_groups = [      'address_group',      'domain_group',      'network_group', -    'port_group' +    'port_group', +    'interface_group'  ]  nested_group_types = [ diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 00539b9c7..40a32efb3 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -15,7 +15,6 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import re  from json import loads  from sys import exit @@ -25,7 +24,6 @@ from vyos.config import Config  from vyos.template import render  from vyos.util import cmd  from vyos.util import dict_search_args -from vyos.util import dict_search_recursive  from vyos.util import run  from vyos import ConfigError  from vyos import airbag @@ -34,48 +32,14 @@ airbag.enable()  mark_offset = 0x7FFFFFFF  nftables_conf = '/run/nftables_policy.conf' -ROUTE_PREFIX = 'VYOS_PBR_' -ROUTE6_PREFIX = 'VYOS_PBR6_' - -preserve_chains = [ -    'VYOS_PBR_PREROUTING', -    'VYOS_PBR_POSTROUTING', -    'VYOS_PBR6_PREROUTING', -    'VYOS_PBR6_POSTROUTING' -] -  valid_groups = [      'address_group', +    'domain_group',      'network_group', -    'port_group' +    'port_group', +    'interface_group'  ] -group_set_prefix = { -    'A_': 'address_group', -    'A6_': 'ipv6_address_group', -#    'D_': 'domain_group', -    'M_': 'mac_group', -    'N_': 'network_group', -    'N6_': 'ipv6_network_group', -    'P_': 'port_group' -} - -def get_policy_interfaces(conf): -    out = {} -    interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, -                                    no_tag_node_value_mangle=True) -    def find_interfaces(iftype_conf, output={}, prefix=''): -        for ifname, if_conf in iftype_conf.items(): -            if 'policy' in if_conf: -                output[prefix + ifname] = if_conf['policy'] -            for vif in ['vif', 'vif_s', 'vif_c']: -                if vif in if_conf: -                    output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) -        return output -    for iftype, iftype_conf in interfaces.items(): -        out.update(find_interfaces(iftype_conf)) -    return out -  def get_config(config=None):      if config:          conf = config @@ -88,7 +52,6 @@ def get_config(config=None):      policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,                                      no_tag_node_value_mangle=True) -    policy['interfaces'] = get_policy_interfaces(conf)      return policy @@ -132,8 +95,8 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):              side_conf = rule_conf[side]              if 'group' in side_conf: -                if {'address_group', 'network_group'} <= set(side_conf['group']): -                    raise ConfigError('Only one address-group or network-group can be specified') +                if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1: +                    raise ConfigError('Only one address-group, domain-group or network-group can be specified')                  for group in valid_groups:                      if group in side_conf['group']: @@ -168,73 +131,11 @@ def verify(policy):                      for rule_id, rule_conf in pol_conf['rule'].items():                          verify_rule(policy, name, rule_conf, ipv6, rule_id) -    for ifname, if_policy in policy['interfaces'].items(): -        name = dict_search_args(if_policy, 'route') -        ipv6_name = dict_search_args(if_policy, 'route6') - -        if name and not dict_search_args(policy, 'route', name): -            raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}') - -        if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name): -            raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}') -      return None -def cleanup_commands(policy): -    commands = [] -    commands_chains = [] -    commands_sets = [] -    for table in ['ip mangle', 'ip6 mangle']: -        route_node = 'route' if table == 'ip mangle' else 'route6' -        chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX - -        json_str = cmd(f'nft -t -j list table {table}') -        obj = loads(json_str) -        if 'nftables' not in obj: -            continue -        for item in obj['nftables']: -            if 'chain' in item: -                chain = item['chain']['name'] -                if chain in preserve_chains or not chain.startswith("VYOS_PBR"): -                    continue - -                if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: -                    commands.append(f'flush chain {table} {chain}') -                else: -                    commands_chains.append(f'delete chain {table} {chain}') - -            if 'rule' in item: -                rule = item['rule'] -                chain = rule['chain'] -                handle = rule['handle'] - -                if chain not in preserve_chains: -                    continue - -                target, _ = next(dict_search_recursive(rule['expr'], 'target')) - -                if target.startswith(chain_prefix): -                    if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: -                        commands.append(f'delete rule {table} {chain} handle {handle}') - -            if 'set' in item: -                set_name = item['set']['name'] - -                for prefix, group_type in group_set_prefix.items(): -                    if set_name.startswith(prefix): -                        group_name = set_name.replace(prefix, "", 1) -                        if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: -                            commands_sets.append(f'flush set {table} {set_name}') -                        else: -                            commands_sets.append(f'delete set {table} {set_name}') - -    return commands + commands_chains + commands_sets -  def generate(policy):      if not os.path.exists(nftables_conf):          policy['first_install'] = True -    else: -        policy['cleanup_commands'] = cleanup_commands(policy)      render(nftables_conf, 'firewall/nftables-policy.j2', policy)      return None diff --git a/src/migration-scripts/firewall/8-to-9 b/src/migration-scripts/firewall/8-to-9 new file mode 100755 index 000000000..f7c1bb90d --- /dev/null +++ b/src/migration-scripts/firewall/8-to-9 @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T4780: Add firewall interface group +#  cli changes from:        +#  set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface] <interface_name> +#  To +#  set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface]  [interface-name | interface-group] <interface_name | interface_group> + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +if config.exists(base + ['name']): +    for name in config.list_nodes(base + ['name']): +        if not config.exists(base + ['name', name, 'rule']): +            continue + +        for rule in config.list_nodes(base + ['name', name, 'rule']): +            rule_iiface = base + ['name', name, 'rule', rule, 'inbound-interface'] +            rule_oiface = base + ['name', name, 'rule', rule, 'outbound-interface'] + +            if config.exists(rule_iiface): +                tmp = config.return_value(rule_iiface) +                config.delete(rule_iiface) +                config.set(rule_iiface + ['interface-name'], value=tmp) + +            if config.exists(rule_oiface): +                tmp = config.return_value(rule_oiface) +                config.delete(rule_oiface) +                config.set(rule_oiface + ['interface-name'], value=tmp) + + +if config.exists(base + ['ipv6-name']): +    for name in config.list_nodes(base + ['ipv6-name']): +        if not config.exists(base + ['ipv6-name', name, 'rule']): +            continue + +        for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): +            rule_iiface = base + ['ipv6-name', name, 'rule', rule, 'inbound-interface'] +            rule_oiface = base + ['ipv6-name', name, 'rule', rule, 'outbound-interface'] + +            if config.exists(rule_iiface): +                tmp = config.return_value(rule_iiface) +                config.delete(rule_iiface) +                config.set(rule_iiface + ['interface-name'], value=tmp) + +            if config.exists(rule_oiface): +                tmp = config.return_value(rule_oiface) +                config.delete(rule_oiface) +                config.set(rule_oiface + ['interface-name'], value=tmp) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1)
\ No newline at end of file | 
