diff options
| -rw-r--r-- | data/templates/firewall/nftables-zone.j2 | 72 | ||||
| -rw-r--r-- | data/templates/firewall/nftables.j2 | 15 | ||||
| -rw-r--r-- | data/templates/zone_policy/nftables.j2 | 77 | ||||
| -rw-r--r-- | data/templates/zone_policy/nftables6.j2 | 77 | ||||
| -rw-r--r-- | interface-definitions/firewall.xml.in | 137 | ||||
| -rw-r--r-- | interface-definitions/zone-policy.xml.in | 148 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 30 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_zone_policy.py | 69 | ||||
| -rwxr-xr-x | src/conf_mode/firewall.py | 117 | ||||
| -rwxr-xr-x | src/conf_mode/zone_policy.py | 192 | ||||
| -rwxr-xr-x | src/migration-scripts/firewall/7-to-8 | 12 | 
11 files changed, 333 insertions, 613 deletions
| diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2 new file mode 100644 index 000000000..919881e19 --- /dev/null +++ b/data/templates/firewall/nftables-zone.j2 @@ -0,0 +1,72 @@ + +{% macro zone_chains(zone, state_policy=False, ipv6=False) %} +{% set fw_name = 'ipv6_name' if ipv6 else 'name' %} +{% set suffix = '6' if ipv6 else '' %} +    chain VYOS_ZONE_FORWARD { +        type filter hook forward priority 1; policy accept; +{% if state_policy %} +        jump VYOS_STATE_POLICY{{ suffix }} +{% endif %} +{% for zone_name, zone_conf in zone.items() %} +{%     if 'local_zone' not in zone_conf %} +        oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }} +{%     endif %} +{% endfor %} +    } +    chain VYOS_ZONE_LOCAL { +        type filter hook input priority 1; policy accept; +{% if state_policy %} +        jump VYOS_STATE_POLICY{{ suffix }} +{% endif %} +{% for zone_name, zone_conf in zone.items() %} +{%     if 'local_zone' in zone_conf %} +        counter jump VZONE_{{ zone_name }}_IN +{%     endif %} +{% endfor %} +    } +    chain VYOS_ZONE_OUTPUT { +        type filter hook output priority 1; policy accept; +{% if state_policy %} +        jump VYOS_STATE_POLICY{{ suffix }} +{% endif %} +{% for zone_name, zone_conf in zone.items() %} +{%     if 'local_zone' in zone_conf %} +        counter jump VZONE_{{ zone_name }}_OUT +{%     endif %} +{% endfor %} +    } +{% for zone_name, zone_conf in zone.items() %} +{%     if zone_conf.local_zone is vyos_defined %} +    chain VZONE_{{ zone_name }}_IN { +        iifname lo counter return +{%         for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} +        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} +        iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{%         endfor %} +        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} +    } +    chain VZONE_{{ zone_name }}_OUT { +        oifname lo counter return +{%         for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall[fw_name] is vyos_defined %} +        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} +        oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{%         endfor %} +        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} +    } +{%     else %} +    chain VZONE_{{ zone_name }} { +        iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6) }} +{%         if zone_conf.intra_zone_filtering is vyos_defined %} +        iifname { {{ zone_conf.interface | join(",") }} } counter return +{%         endif %} +{%         for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} +{%             if zone[from_zone].local_zone is not defined %} +        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} +        iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{%             endif %} +{%         endfor %} +        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} +    } +{%     endif %} +{% endfor %} +{% endmacro %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index dde88d09d..c0780dad5 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -1,6 +1,7 @@  #!/usr/sbin/nft -f  {% import 'firewall/nftables-defines.j2' as group_tmpl %} +{% import 'firewall/nftables-zone.j2' as zone_tmpl %}  {% if first_install is not vyos_defined %}  delete table ip vyos_filter @@ -93,6 +94,10 @@ table ip vyos_filter {  {{ group_tmpl.groups(group, False) }} +{% if zone is vyos_defined %} +{{ zone_tmpl.zone_chains(zone, state_policy is vyos_defined, False) }} +{% endif %} +  {% if state_policy is vyos_defined %}      chain VYOS_STATE_POLICY {  {%     if state_policy.established is vyos_defined %} @@ -107,9 +112,6 @@ table ip vyos_filter {          return      }  {% endif %} -{% if zone_conf is vyos_defined %} -    include "{{ zone_conf }}" -{% endif %}  }  {% if first_install is not vyos_defined %} @@ -195,6 +197,10 @@ table ip6 vyos_filter {  {{ group_tmpl.groups(group, True) }} +{% if zone is vyos_defined %} +{{ zone_tmpl.zone_chains(zone, state_policy is vyos_defined, True) }} +{% endif %} +  {% if state_policy is vyos_defined %}      chain VYOS_STATE_POLICY6 {  {%     if state_policy.established is vyos_defined %} @@ -209,7 +215,4 @@ table ip6 vyos_filter {          return      }  {% endif %} -{% if zone6_conf is vyos_defined %} -    include "{{ zone6_conf }}" -{% endif %}  } diff --git a/data/templates/zone_policy/nftables.j2 b/data/templates/zone_policy/nftables.j2 deleted file mode 100644 index 09140519f..000000000 --- a/data/templates/zone_policy/nftables.j2 +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/sbin/nft -f - -{% if zone is vyos_defined %} -    chain VYOS_ZONE_FORWARD { -        type filter hook forward priority -1; policy accept; -{%     if firewall.state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv4 %} -{%             if 'local_zone' not in zone_conf %} -        oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }} -{%             endif %} -{%         endif %} -{%     endfor %} -    } -    chain VYOS_ZONE_LOCAL { -        type filter hook input priority -1; policy accept; -{%     if firewall.state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv4 %} -{%             if 'local_zone' in zone_conf %} -        counter jump VZONE_{{ zone_name }}_IN -{%             endif %} -{%         endif %} -{%     endfor %} -    } -    chain VYOS_ZONE_OUTPUT { -        type filter hook output priority -1; policy accept; -{%     if firewall.state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv4 %} -{%             if 'local_zone' in zone_conf %} -        counter jump VZONE_{{ zone_name }}_OUT -{%             endif %} -{%         endif %} -{%     endfor %} -    } -{%     for zone_name, zone_conf in zone.items() if zone_conf.ipv4 %} -{%         if zone_conf.local_zone is vyos_defined %} -    chain VZONE_{{ zone_name }}_IN { -        iifname lo counter return -{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is vyos_defined %} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} -    } -    chain VZONE_{{ zone_name }}_OUT { -        oifname lo counter return -{%             for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is vyos_defined %} -        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }} -        oifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} -    } -{%         else %} -    chain VZONE_{{ zone_name }} { -        iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=False) }} -{%             if zone_conf.intra_zone_filtering is vyos_defined %} -        iifname { {{ zone_conf.interface | join(",") }} } counter return -{%             endif %} -{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is vyos_defined %} -{%                 if zone[from_zone].local_zone is not defined %} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%                 endif %} -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone_' + zone_name) }} -    } -{%         endif %} -{%     endfor %} -{% endif %} diff --git a/data/templates/zone_policy/nftables6.j2 b/data/templates/zone_policy/nftables6.j2 deleted file mode 100644 index f7123d63d..000000000 --- a/data/templates/zone_policy/nftables6.j2 +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/sbin/nft -f - -{% if zone is vyos_defined %} -    chain VYOS_ZONE6_FORWARD { -        type filter hook forward priority -1; policy accept; -{%     if state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY6 -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv6 %} -{%             if 'local_zone' not in zone_conf %} -        oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }} -{%             endif %} -{%         endif %} -{%     endfor %} -    } -    chain VYOS_ZONE6_LOCAL { -        type filter hook input priority -1; policy accept; -{%     if firewall.state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY6 -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv6 %} -{%             if 'local_zone' in zone_conf %} -        counter jump VZONE6_{{ zone_name }}_IN -{%             endif %} -{%         endif %} -{%     endfor %} -    } -    chain VYOS_ZONE6_OUTPUT { -        type filter hook output priority -1; policy accept; -{%     if firewall.state_policy is vyos_defined %} -        jump VYOS_STATE_POLICY6 -{%     endif %} -{%     for zone_name, zone_conf in zone.items() %} -{%         if zone_conf.ipv6 %} -{%             if 'local_zone' in zone_conf %} -        counter jump VZONE6_{{ zone_name }}_OUT -{%             endif %} -{%         endif %} -{%     endfor %} -    } -{%     for zone_name, zone_conf in zone.items() if zone_conf.ipv6 %} -{%         if zone_conf.local_zone is vyos_defined %} -    chain VZONE6_{{ zone_name }}_IN { -        iifname lo counter return -{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is vyos_defined %} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} -    } -    chain VZONE6_{{ zone_name }}_OUT { -        oifname lo counter return -{%             for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is vyos_defined %} -        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }} -        oifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} -    } -{%         else %} -    chain VZONE6_{{ zone_name }} { -        iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=True) }} -{%             if zone_conf.intra_zone_filtering is vyos_defined %} -        iifname { {{ zone_conf.interface | join(",") }} } counter return -{%             endif %} -{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is vyos_defined %} -{%                 if zone[from_zone].local_zone is not defined %} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }} -        iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%                 endif %} -{%             endfor %} -        {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} -    } -{%         endif %} -{%     endfor %} -{% endif %} diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index fb24cd558..d39dddc77 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -742,6 +742,143 @@          </properties>          <defaultValue>disable</defaultValue>        </leafNode> +      <tagNode name="zone"> +        <properties> +          <help>Zone-policy</help> +          <valueHelp> +            <format>txt</format> +            <description>Zone name</description> +          </valueHelp> +          <constraint> +            <regex>[a-zA-Z0-9][\w\-\.]*</regex> +          </constraint> +        </properties> +        <children> +          #include <include/generic-description.xml.i> +          #include <include/firewall/enable-default-log.xml.i> +          <leafNode name="default-action"> +            <properties> +              <help>Default-action for traffic coming into this zone</help> +              <completionHelp> +                <list>drop reject</list> +              </completionHelp> +              <valueHelp> +                <format>drop</format> +                <description>Drop silently</description> +              </valueHelp> +              <valueHelp> +                <format>reject</format> +                <description>Drop and notify source</description> +              </valueHelp> +              <constraint> +                <regex>(drop|reject)</regex> +              </constraint> +            </properties> +            <defaultValue>drop</defaultValue> +          </leafNode> +          <tagNode name="from"> +            <properties> +              <help>Zone from which to filter traffic</help> +              <completionHelp> +                <path>zone-policy zone</path> +              </completionHelp> +            </properties> +            <children> +              <node name="firewall"> +                <properties> +                  <help>Firewall options</help> +                </properties> +                <children> +                  <leafNode name="ipv6-name"> +                    <properties> +                      <help>IPv6 firewall ruleset</help> +                      <completionHelp> +                        <path>firewall ipv6-name</path> +                      </completionHelp> +                    </properties> +                  </leafNode> +                  <leafNode name="name"> +                    <properties> +                      <help>IPv4 firewall ruleset</help> +                      <completionHelp> +                        <path>firewall name</path> +                      </completionHelp> +                    </properties> +                  </leafNode> +                </children> +              </node> +            </children> +          </tagNode> +          <leafNode name="interface"> +            <properties> +              <help>Interface associated with zone</help> +              <valueHelp> +                <format>txt</format> +                <description>Interface associated with zone</description> +              </valueHelp> +              <completionHelp> +                <script>${vyos_completion_dir}/list_interfaces.py</script> +              </completionHelp> +              <multi/> +            </properties> +          </leafNode> +          <node name="intra-zone-filtering"> +            <properties> +              <help>Intra-zone filtering</help> +            </properties> +            <children> +              <leafNode name="action"> +                <properties> +                  <help>Action for intra-zone traffic</help> +                  <completionHelp> +                    <list>accept drop</list> +                  </completionHelp> +                  <valueHelp> +                    <format>accept</format> +                    <description>Accept traffic</description> +                  </valueHelp> +                  <valueHelp> +                    <format>drop</format> +                    <description>Drop silently</description> +                  </valueHelp> +                  <constraint> +                    <regex>(accept|drop)</regex> +                  </constraint> +                </properties> +              </leafNode> +              <node name="firewall"> +                <properties> +                  <help>Use the specified firewall chain</help> +                </properties> +                <children> +                  <leafNode name="ipv6-name"> +                    <properties> +                      <help>IPv6 firewall ruleset</help> +                      <completionHelp> +                        <path>firewall ipv6-name</path> +                      </completionHelp> +                    </properties> +                  </leafNode> +                  <leafNode name="name"> +                    <properties> +                      <help>IPv4 firewall ruleset</help> +                      <completionHelp> +                        <path>firewall name</path> +                      </completionHelp> +                    </properties> +                  </leafNode> +                </children> +              </node> +            </children> +          </node> +          <leafNode name="local-zone"> +            <properties> +              <help>Zone to be local-zone</help> +              <valueless/> +            </properties> +          </leafNode> +        </children> +      </tagNode>      </children>    </node>  </interfaceDefinition> diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in deleted file mode 100644 index cf53e2bc8..000000000 --- a/interface-definitions/zone-policy.xml.in +++ /dev/null @@ -1,148 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> -  <node name="zone-policy" owner="${vyos_conf_scripts_dir}/zone_policy.py"> -    <properties> -      <help>Configure zone-policy</help> -      <priority>198</priority> -    </properties> -    <children> -      <tagNode name="zone"> -        <properties> -          <help>Zone name</help> -          <valueHelp> -            <format>txt</format> -            <description>Zone name</description> -          </valueHelp> -          <constraint> -            <regex>[a-zA-Z0-9][\w\-\.]*</regex> -          </constraint> -        </properties> -        <children> -          #include <include/generic-description.xml.i> -          #include <include/firewall/enable-default-log.xml.i> -          <leafNode name="default-action"> -            <properties> -              <help>Default-action for traffic coming into this zone</help> -              <completionHelp> -                <list>drop reject</list> -              </completionHelp> -              <valueHelp> -                <format>drop</format> -                <description>Drop silently</description> -              </valueHelp> -              <valueHelp> -                <format>reject</format> -                <description>Drop and notify source</description> -              </valueHelp> -              <constraint> -                <regex>(drop|reject)</regex> -              </constraint> -            </properties> -            <defaultValue>drop</defaultValue> -          </leafNode> -          <tagNode name="from"> -            <properties> -              <help>Zone from which to filter traffic</help> -              <completionHelp> -                <path>zone-policy zone</path> -              </completionHelp> -            </properties> -            <children> -              <node name="firewall"> -                <properties> -                  <help>Firewall options</help> -                </properties> -                <children> -                  <leafNode name="ipv6-name"> -                    <properties> -                      <help>IPv6 firewall ruleset</help> -                      <completionHelp> -                        <path>firewall ipv6-name</path> -                      </completionHelp> -                    </properties> -                  </leafNode> -                  <leafNode name="name"> -                    <properties> -                      <help>IPv4 firewall ruleset</help> -                      <completionHelp> -                        <path>firewall name</path> -                      </completionHelp> -                    </properties> -                  </leafNode> -                </children> -              </node> -            </children> -          </tagNode> -          <leafNode name="interface"> -            <properties> -              <help>Interface associated with zone</help> -              <valueHelp> -                <format>txt</format> -                <description>Interface associated with zone</description> -              </valueHelp> -              <completionHelp> -                <script>${vyos_completion_dir}/list_interfaces.py</script> -              </completionHelp> -              <multi/> -            </properties> -          </leafNode> -          <node name="intra-zone-filtering"> -            <properties> -              <help>Intra-zone filtering</help> -            </properties> -            <children> -              <leafNode name="action"> -                <properties> -                  <help>Action for intra-zone traffic</help> -                  <completionHelp> -                    <list>accept drop</list> -                  </completionHelp> -                  <valueHelp> -                    <format>accept</format> -                    <description>Accept traffic</description> -                  </valueHelp> -                  <valueHelp> -                    <format>drop</format> -                    <description>Drop silently</description> -                  </valueHelp> -                  <constraint> -                    <regex>(accept|drop)</regex> -                  </constraint> -                </properties> -              </leafNode> -              <node name="firewall"> -                <properties> -                  <help>Use the specified firewall chain</help> -                </properties> -                <children> -                  <leafNode name="ipv6-name"> -                    <properties> -                      <help>IPv6 firewall ruleset</help> -                      <completionHelp> -                        <path>firewall ipv6-name</path> -                      </completionHelp> -                    </properties> -                  </leafNode> -                  <leafNode name="name"> -                    <properties> -                      <help>IPv4 firewall ruleset</help> -                      <completionHelp> -                        <path>firewall name</path> -                      </completionHelp> -                    </properties> -                  </leafNode> -                </children> -              </node> -            </children> -          </node> -          <leafNode name="local-zone"> -            <properties> -              <help>Zone to be local-zone</help> -              <valueless/> -            </properties> -          </leafNode> -        </children> -      </tagNode> -    </children> -  </node> -</interfaceDefinition> diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 218062ef1..0ca2407e4 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -390,5 +390,35 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):                  with open(path, 'r') as f:                      self.assertNotEqual(f.read().strip(), conf['default'], msg=path) +    def test_zone_basic(self): +        self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) +        self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0']) +        self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) +        self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone']) +        self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) + +        self.cli_commit() + +        nftables_search = [ +            ['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'] +        ] + +        nftables_output = cmd('sudo nft list table ip vyos_filter') + +        for search in nftables_search: +            matched = False +            for line in nftables_output.split("\n"): +                if all(item in line for item in search): +                    matched = True +                    break +            self.assertTrue(matched) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py deleted file mode 100755 index 8add589e8..000000000 --- a/smoketest/scripts/cli/test_zone_policy.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-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/>. - -import unittest - -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.util import cmd - -class TestZonePolicy(VyOSUnitTestSHIM.TestCase): -    @classmethod -    def setUpClass(cls): -        super(TestZonePolicy, cls).setUpClass() -        cls.cli_set(cls, ['firewall', 'name', 'smoketest', 'default-action', 'drop']) - -    @classmethod -    def tearDownClass(cls): -        cls.cli_delete(cls, ['firewall']) -        super(TestZonePolicy, cls).tearDownClass() - -    def tearDown(self): -        self.cli_delete(['zone-policy']) -        self.cli_commit() - -    def test_basic_zone(self): -        self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'interface', 'eth0']) -        self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) -        self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'local-zone']) -        self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) - -        self.cli_commit() - -        nftables_search = [ -            ['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'] -        ] - -        nftables_output = cmd('sudo nft list table ip vyos_filter') - -        for search in nftables_search: -            matched = False -            for line in nftables_output.split("\n"): -                if all(item in line for item in search): -                    matched = True -                    break -            self.assertTrue(matched) - - -if __name__ == '__main__': -    unittest.main(verbosity=2) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f8ad1f798..eeb57bd30 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -48,8 +48,6 @@ airbag.enable()  policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'  nftables_conf = '/run/nftables.conf' -nftables_zone_conf = '/run/nftables_zone.conf' -nftables6_zone_conf = '/run/nftables_zone6.conf'  sysfs_config = {      'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, @@ -87,37 +85,6 @@ snmp_event_source = 1  snmp_trap_mib = 'VYATTA-TRAP-MIB'  snmp_trap_name = 'mgmtEventTrap' -def get_firewall_zones(conf): -    used_v4 = [] -    used_v6 = [] -    zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True, -                                    no_tag_node_value_mangle=True) - -    if 'zone' in zone_policy: -        for zone, zone_conf in zone_policy['zone'].items(): -            if 'from' in zone_conf: -                for from_zone, from_conf in zone_conf['from'].items(): -                    name = dict_search_args(from_conf, 'firewall', 'name') -                    if name: -                        used_v4.append(name) - -                    ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') -                    if ipv6_name: -                        used_v6.append(ipv6_name) - -            if 'intra_zone_filtering' in zone_conf: -                name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name') -                if name: -                    used_v4.append(name) - -                ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') -                if ipv6_name: -                    used_v6.append(ipv6_name) -    else: -        return None - -    return {'name': used_v4, 'ipv6_name': used_v6} -  def geoip_updated(conf, firewall):      diff = get_config_diff(conf)      node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) @@ -171,6 +138,9 @@ def get_config(config=None):          if tmp in default_values:              del default_values[tmp] +    if 'zone' in default_values: +        del default_values['zone'] +      firewall = dict_merge(default_values, firewall)      # Merge in defaults for IPv4 ruleset @@ -187,8 +157,12 @@ def get_config(config=None):              firewall['ipv6_name'][ipv6_name] = dict_merge(default_values,                                                            firewall['ipv6_name'][ipv6_name]) +    if 'zone' in firewall: +        default_values = defaults(base + ['zone']) +        for zone in firewall['zone']: +            firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) +      firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) -    firewall['zone_policy'] = get_firewall_zones(conf)      if 'config_trap' in firewall and firewall['config_trap'] == 'enable':          diff = get_config_diff(conf) @@ -332,11 +306,61 @@ def verify(firewall):                  if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:                      raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}') -    if firewall['zone_policy']: -        for fw_name, used_names in firewall['zone_policy'].items(): -            for name in used_names: -                if dict_search_args(firewall, fw_name, name) == None: -                    raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') +    local_zone = False +    zone_interfaces = [] + +    if 'zone' in firewall: +        for zone, zone_conf in firewall['zone'].items(): +            if 'local_zone' not in zone_conf and 'interface' not in zone_conf: +                raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') + +            if 'local_zone' in zone_conf: +                if local_zone: +                    raise ConfigError('There cannot be multiple local zones') +                if 'interface' in zone_conf: +                    raise ConfigError('Local zone cannot have interfaces assigned') +                if 'intra_zone_filtering' in zone_conf: +                    raise ConfigError('Local zone cannot use intra-zone-filtering') +                local_zone = True + +            if 'interface' in zone_conf: +                found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] + +                if found_duplicates: +                    raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + +                zone_interfaces += zone_conf['interface'] + +            if 'intra_zone_filtering' in zone_conf: +                intra_zone = zone_conf['intra_zone_filtering'] + +                if len(intra_zone) > 1: +                    raise ConfigError('Only one intra-zone-filtering action must be specified') + +                if 'firewall' in intra_zone: +                    v4_name = dict_search_args(intra_zone, 'firewall', 'name') +                    if v4_name and not dict_search_args(firewall, 'name', v4_name): +                        raise ConfigError(f'Firewall name "{v4_name}" does not exist') + +                    v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name') +                    if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): +                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + +                    if not v4_name and not v6_name: +                        raise ConfigError('No firewall names specified for intra-zone-filtering') + +            if 'from' in zone_conf: +                for from_zone, from_conf in zone_conf['from'].items(): +                    if from_zone not in firewall['zone']: +                        raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') + +                    v4_name = dict_search_args(from_conf, 'firewall', 'name') +                    if v4_name and not dict_search_args(firewall, 'name', v4_name): +                        raise ConfigError(f'Firewall name "{v4_name}" does not exist') + +                    v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') +                    if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): +                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')      return None @@ -344,11 +368,18 @@ def generate(firewall):      if not os.path.exists(nftables_conf):          firewall['first_install'] = True -    if os.path.exists(nftables_zone_conf): -        firewall['zone_conf'] = nftables_zone_conf +    if 'zone' in firewall: +        for local_zone, local_zone_conf in firewall['zone'].items(): +            if 'local_zone' not in local_zone_conf: +                continue + +            local_zone_conf['from_local'] = {} -    if os.path.exists(nftables6_zone_conf): -        firewall['zone6_conf'] = nftables6_zone_conf +            for zone, zone_conf in firewall['zone'].items(): +                if zone == local_zone or 'from' not in zone_conf: +                    continue +                if local_zone in zone_conf['from']: +                    local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]      render(nftables_conf, 'firewall/nftables.j2', firewall)      return None diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py deleted file mode 100755 index c6ab4e304..000000000 --- a/src/conf_mode/zone_policy.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-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/>. - -import os - -from json import loads -from sys import exit - -from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdiff import get_config_diff -from vyos.template import render -from vyos.util import cmd -from vyos.util import dict_search_args -from vyos.util import run -from vyos.xml import defaults -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -firewall_conf_script = '/usr/libexec/vyos/conf_mode/firewall.py' -nftables_conf = '/run/nftables_zone.conf' -nftables6_conf = '/run/nftables_zone6.conf' - -def get_config(config=None): -    if config: -        conf = config -    else: -        conf = Config() -    base = ['zone-policy'] -    zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), -                                       get_first_key=True, -                                       no_tag_node_value_mangle=True) - -    zone_policy['firewall'] = conf.get_config_dict(['firewall'], -                                                   key_mangling=('-', '_'), -                                                   get_first_key=True, -                                                   no_tag_node_value_mangle=True) - -    diff = get_config_diff(conf) -    zone_policy['firewall_changed'] = diff.is_node_changed(['firewall']) - -    if 'zone' in zone_policy: -        # We have gathered the dict representation of the CLI, but there are default -        # options which we need to update into the dictionary retrived. -        default_values = defaults(base + ['zone']) -        for zone in zone_policy['zone']: -            zone_policy['zone'][zone] = dict_merge(default_values, -                                                   zone_policy['zone'][zone]) - -    return zone_policy - -def verify(zone_policy): -    # bail out early - looks like removal from running config -    if not zone_policy: -        return None - -    local_zone = False -    interfaces = [] - -    if 'zone' in zone_policy: -        for zone, zone_conf in zone_policy['zone'].items(): -            if 'local_zone' not in zone_conf and 'interface' not in zone_conf: -                raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') - -            if 'local_zone' in zone_conf: -                if local_zone: -                    raise ConfigError('There cannot be multiple local zones') -                if 'interface' in zone_conf: -                    raise ConfigError('Local zone cannot have interfaces assigned') -                if 'intra_zone_filtering' in zone_conf: -                    raise ConfigError('Local zone cannot use intra-zone-filtering') -                local_zone = True - -            if 'interface' in zone_conf: -                found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces] - -                if found_duplicates: -                    raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - -                interfaces += zone_conf['interface'] - -            if 'intra_zone_filtering' in zone_conf: -                intra_zone = zone_conf['intra_zone_filtering'] - -                if len(intra_zone) > 1: -                    raise ConfigError('Only one intra-zone-filtering action must be specified') - -                if 'firewall' in intra_zone: -                    v4_name = dict_search_args(intra_zone, 'firewall', 'name') -                    if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name): -                        raise ConfigError(f'Firewall name "{v4_name}" does not exist') - -                    v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name') -                    if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name): -                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - -                    if not v4_name and not v6_name: -                        raise ConfigError('No firewall names specified for intra-zone-filtering') - -            if 'from' in zone_conf: -                for from_zone, from_conf in zone_conf['from'].items(): -                    if from_zone not in zone_policy['zone']: -                        raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') - -                    v4_name = dict_search_args(from_conf, 'firewall', 'name') -                    if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name): -                        raise ConfigError(f'Firewall name "{v4_name}" does not exist') - -                    v6_name = dict_search_args(from_conf, 'firewall', 'v6_name') -                    if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): -                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - -    return None - -def has_ipv4_fw(zone_conf): -    if 'from' not in zone_conf: -        return False -    zone_from = zone_conf['from'] -    return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')]) - -def has_ipv6_fw(zone_conf): -    if 'from' not in zone_conf: -        return False -    zone_from = zone_conf['from'] -    return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')]) - -def get_local_from(zone_policy, local_zone_name): -    # Get all zone firewall names from the local zone -    out = {} -    for zone, zone_conf in zone_policy['zone'].items(): -        if zone == local_zone_name: -            continue -        if 'from' not in zone_conf: -            continue -        if local_zone_name in zone_conf['from']: -            out[zone] = zone_conf['from'][local_zone_name] -    return out - -def generate(zone_policy): -    data = zone_policy or {} - -    if not os.path.exists(nftables_conf): -        data['first_install'] = True - -    if 'zone' in data: -        for zone, zone_conf in data['zone'].items(): -            zone_conf['ipv4'] = has_ipv4_fw(zone_conf) -            zone_conf['ipv6'] = has_ipv6_fw(zone_conf) - -            if 'local_zone' in zone_conf: -                zone_conf['from_local'] = get_local_from(data, zone) - -    render(nftables_conf, 'zone_policy/nftables.j2', data) -    render(nftables6_conf, 'zone_policy/nftables6.j2', data) -    return None - -def update_firewall(): -    # Update firewall to refresh nftables -    tmp = run(firewall_conf_script) -    if tmp > 0: -        Warning('Failed to update firewall configuration!') - -def apply(zone_policy): -    # If firewall will not update in this commit, we need to call the conf script -    if not zone_policy['firewall_changed']: -        update_firewall() - -    return None - -if __name__ == '__main__': -    try: -        c = get_config() -        verify(c) -        generate(c) -        apply(c) -    except ConfigError as e: -        print(e) -        exit(1) diff --git a/src/migration-scripts/firewall/7-to-8 b/src/migration-scripts/firewall/7-to-8 index 6929e20a5..ce527acf5 100755 --- a/src/migration-scripts/firewall/7-to-8 +++ b/src/migration-scripts/firewall/7-to-8 @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  # T2199: Migrate interface firewall nodes to firewall interfaces <ifname> <direction> name/ipv6-name <name> +# T2199: Migrate zone-policy to firewall node  import re @@ -34,9 +35,10 @@ with open(file_name, 'r') as f:      config_file = f.read()  base = ['firewall'] +zone_base = ['zone-policy']  config = ConfigTree(config_file) -if not config.exists(base): +if not config.exists(base) and not config.exists(zone_base):      # Nothing to do      exit(0) @@ -80,6 +82,14 @@ for iftype in config.list_nodes(['interfaces']):                      for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']):                          migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc) +if config.exists(zone_base + ['zone']): +    config.set(['firewall', 'zone']) +    config.set_tag(['firewall', 'zone']) + +    for zone in config.list_nodes(zone_base + ['zone']): +        config.copy(zone_base + ['zone', zone], ['firewall', 'zone', zone]) +    config.delete(zone_base) +  try:      with open(file_name, 'w') as f:          f.write(config.to_string()) | 
