diff options
22 files changed, 644 insertions, 475 deletions
diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2 new file mode 100644 index 000000000..1e9351f97 --- /dev/null +++ b/data/templates/firewall/nftables-zone.j2 @@ -0,0 +1,69 @@ + +{% macro zone_chains(zone, 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; +{% 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; +{% 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; +{% 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 +{% if zone_conf.from is vyos_defined %} +{% 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 %} +{% endif %} + {{ zone_conf | nft_default_rule('zone_' + zone_name) }} + } + chain VZONE_{{ zone_name }}_OUT { + oifname lo counter return +{% if zone_conf.from_local is vyos_defined %} +{% 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 %} +{% endif %} + {{ 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 %} +{% if zone_conf.from is vyos_defined %} +{% 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 %} +{% endif %} + {{ zone_conf | nft_default_rule('zone_' + zone_name) }} + } +{% endif %} +{% endfor %} +{% endmacro %}
\ No newline at end of file diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 75800ee3d..e24a9655d 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -3,6 +3,7 @@ {% 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 @@ -43,9 +44,8 @@ table ip vyos_filter { {% set ns = namespace(sets=[]) %} {% if ipv4.forward is vyos_defined %} {% for prior, conf in ipv4.forward.items() %} -{% set def_action = conf.default_action %} chain VYOS_FORWARD_{{ prior }} { - type filter hook forward priority {{ prior }}; policy {{ def_action }}; + type filter hook forward 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('FWD', prior, rule_id) }} @@ -54,15 +54,15 @@ table ip vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('FWD-filter') }} } {% endfor %} {% endif %} {% if ipv4.input is vyos_defined %} {% for prior, conf in ipv4.input.items() %} -{% set def_action = conf.default_action %} chain VYOS_INPUT_{{ prior }} { - type filter hook input priority {{ prior }}; policy {{ def_action }}; + type filter hook input 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('INP',prior, rule_id) }} @@ -71,15 +71,15 @@ table ip vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('INP-filter') }} } {% endfor %} {% endif %} {% if ipv4.output is vyos_defined %} {% for prior, conf in ipv4.output.items() %} -{% set def_action = conf.default_action %} chain VYOS_OUTPUT_{{ prior }} { - type filter hook output priority {{ prior }}; policy {{ def_action }}; + type filter hook output 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('OUT', prior, rule_id) }} @@ -88,6 +88,7 @@ table ip vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('OUT-filter') }} } {% endfor %} {% endif %} @@ -97,9 +98,8 @@ table ip vyos_filter { } {% if ipv4.prerouting is vyos_defined %} {% for prior, conf in ipv4.prerouting.items() %} -{% set def_action = conf.default_action %} chain VYOS_PREROUTING_{{ prior }} { - type filter hook prerouting priority {{ prior }}; policy {{ def_action }}; + 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) }} @@ -108,7 +108,7 @@ table ip vyos_filter { {% endif %} {% endfor %} {% endif %} - {{ conf | nft_default_rule(prior) }} + {{ conf | nft_default_rule('PRE-filter') }} } {% endfor %} {% endif %} @@ -152,6 +152,10 @@ table ip vyos_filter { {% endif %} {% endif %} {{ group_tmpl.groups(group, False, True) }} + +{% if zone is vyos_defined %} +{{ zone_tmpl.zone_chains(zone, False) }} +{% endif %} } {% if first_install is not vyos_defined %} @@ -168,9 +172,8 @@ table ip6 vyos_filter { {% set ns = namespace(sets=[]) %} {% if ipv6.forward is vyos_defined %} {% for prior, conf in ipv6.forward.items() %} -{% set def_action = conf.default_action %} chain VYOS_IPV6_FORWARD_{{ prior }} { - type filter hook forward priority {{ prior }}; policy {{ def_action }}; + type filter hook forward 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('FWD', prior, rule_id ,'ip6') }} @@ -179,15 +182,15 @@ table ip6 vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('FWD-filter', ipv6=True) }} } {% endfor %} {% endif %} {% if ipv6.input is vyos_defined %} {% for prior, conf in ipv6.input.items() %} -{% set def_action = conf.default_action %} chain VYOS_IPV6_INPUT_{{ prior }} { - type filter hook input priority {{ prior }}; policy {{ def_action }}; + type filter hook input 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('INP', prior, rule_id ,'ip6') }} @@ -196,15 +199,15 @@ table ip6 vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('INP-filter', ipv6=True) }} } {% endfor %} {% endif %} {% if ipv6.output is vyos_defined %} {% for prior, conf in ipv6.output.items() %} -{% set def_action = conf.default_action %} chain VYOS_IPV6_OUTPUT_{{ prior }} { - type filter hook output priority {{ prior }}; policy {{ def_action }}; + type filter hook output 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('OUT', prior, rule_id ,'ip6') }} @@ -213,6 +216,7 @@ table ip6 vyos_filter { {% endif %} {% endfor %} {% endif %} + {{ conf | nft_default_rule('OUT-filter', ipv6=True) }} } {% endfor %} {% endif %} @@ -261,6 +265,9 @@ table ip6 vyos_filter { {% endif %} {% endif %} {{ group_tmpl.groups(group, True, True) }} +{% if zone is vyos_defined %} +{{ zone_tmpl.zone_chains(zone, True) }} +{% endif %} } ## Bridge Firewall @@ -270,4 +277,5 @@ delete table bridge vyos_filter table bridge vyos_filter { {{ bridge_tmpl.bridge(bridge) }} {{ group_tmpl.groups(group, False, False) }} + } diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 81e6b89ea..0bb14a1b3 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -355,6 +355,148 @@ #include <include/firewall/ipv6-custom-name.xml.i> </children> </node> + <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 ipv4 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> + <valueHelp> + <format>vrf</format> + <description>VRF associated with zone</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <path>vrf name</path> + </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 ipv4 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/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 504385b53..3d9333639 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -343,6 +343,7 @@ #include <include/route-map.xml.i> </children> </tagNode> + #include <include/bgp/afi-maximum-paths.xml.i> </children> </node> <node name="ipv4-flowspec"> diff --git a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i index 70c0adb77..100f1c3d9 100644 --- a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/firewall/ipv4-hook-input.xml.i b/interface-definitions/include/firewall/ipv4-hook-input.xml.i index 32b0ec94f..22546640b 100644 --- a/interface-definitions/include/firewall/ipv4-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-input.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/firewall/ipv4-hook-output.xml.i b/interface-definitions/include/firewall/ipv4-hook-output.xml.i index d50d1e93b..80c30cdeb 100644 --- a/interface-definitions/include/firewall/ipv4-hook-output.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-output.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i index d83827161..fb38267eb 100644 --- a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i index e34958f28..49d4493cc 100644 --- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/firewall/ipv6-hook-output.xml.i b/interface-definitions/include/firewall/ipv6-hook-output.xml.i index eb4ea7ac3..452b9027f 100644 --- a/interface-definitions/include/firewall/ipv6-hook-output.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-output.xml.i @@ -10,6 +10,7 @@ </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> diff --git a/interface-definitions/include/version/cluster-version.xml.i b/interface-definitions/include/version/cluster-version.xml.i index 621996df4..402fe36c5 100644 --- a/interface-definitions/include/version/cluster-version.xml.i +++ b/interface-definitions/include/version/cluster-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/cluster-version.xml.i --> -<syntaxVersion component='cluster' version='1'></syntaxVersion> +<syntaxVersion component='cluster' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index fcfb8686c..db3762065 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -123,6 +123,9 @@ <completionHelp> <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> </completionHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> </properties> <children> <leafNode name="native-vlan"> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 71a06b625..075ffe466 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -258,10 +258,10 @@ def has_address_configured(conf, intf): old_level = conf.get_level() conf.set_level([]) - intfpath = 'interfaces ' + Section.get_config_path(intf) - if ( conf.exists(f'{intfpath} address') or - conf.exists(f'{intfpath} ipv6 address autoconf') or - conf.exists(f'{intfpath} ipv6 address eui64') ): + intfpath = ['interfaces', Section.get_config_path(intf)] + if (conf.exists([intfpath, 'address']) or + conf.exists([intfpath, 'ipv6', 'address', 'autoconf']) or + conf.exists([intfpath, 'ipv6', 'address', 'eui64'])): ret = True conf.set_level(old_level) @@ -279,8 +279,7 @@ def has_vrf_configured(conf, intf): old_level = conf.get_level() conf.set_level([]) - tmp = ['interfaces', Section.get_config_path(intf), 'vrf'] - if conf.exists(tmp): + if conf.exists(['interfaces', Section.get_config_path(intf), 'vrf']): ret = True conf.set_level(old_level) @@ -298,8 +297,7 @@ def has_vlan_subinterface_configured(conf, intf): ret = False intfpath = ['interfaces', Section.section(intf), intf] - if ( conf.exists(intfpath + ['vif']) or - conf.exists(intfpath + ['vif-s'])): + if (conf.exists(intfpath + ['vif']) or conf.exists(intfpath + ['vif-s'])): ret = True return ret diff --git a/python/vyos/template.py b/python/vyos/template.py index 3be486cc4..c778d0de8 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -582,10 +582,11 @@ def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'): def nft_default_rule(fw_conf, fw_name, ipv6=False): output = ['counter'] default_action = fw_conf['default_action'] + family = 'ipv6' if ipv6 else 'ipv4' if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() - output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}]"') + output.append(f'log prefix "[{family}-{fw_name[:19]}-default-{action_suffix}]"') #output.append(nft_action(default_action)) output.append(f'{default_action}') diff --git a/scripts/import-conf-mode-commands b/scripts/import-conf-mode-commands deleted file mode 100755 index 996b31c9c..000000000 --- a/scripts/import-conf-mode-commands +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -# -# build-command-template: converts old style commands definitions to XML -# -# Copyright (C) 2019 VyOS maintainers <maintainers@vyos.net> -# -# 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 -# USA - - -import os -import re -import sys - -from lxml import etree - - -# Node types -NODE = 0 -LEAF_NODE = 1 -TAG_NODE = 2 - -def parse_command_data(t): - regs = { - 'help': r'\bhelp:(.*)(?:\n|$)', - 'priority': r'\bpriority:(.*)(?:\n|$)', - 'type': r'\btype:(.*)(?:\n|$)', - 'syntax_expression_var': r'\bsyntax:expression: \$VAR\(\@\) in (.*)' - } - - data = {'multi': False, 'help': ""} - - for r in regs: - try: - data[r] = re.search(regs[r], t).group(1).strip() - except: - data[r] = None - - # val_help is special: there can be multiple instances - val_help_strings = re.findall(r'\bval_help:(.*)(?:\n|$)', t) - val_help = [] - for v in val_help_strings: - try: - fmt, msg = re.match(r'\s*(.*)\s*;\s*(.*)\s*(?:\n|$)', v).groups() - except: - fmt = "<text>" - msg = v - val_help.append((fmt, msg)) - data['val_help'] = val_help - - # multi is on/off - if re.match(r'\bmulti:', t): - data['multi'] = True - - return(data) - -def walk(tree, base_path, name): - path = os.path.join(base_path, name) - - contents = os.listdir(path) - - # Determine node type and create XML element for the node - # Tag node dirs will always have 'node.tag' subdir and 'node.def' file - # Leaf node dirs have nothing but a 'node.def' file - # Everything that doesn't match either of these patterns is a normal node - if 'node.tag' in contents: - print("Creating a tag node from {0}".format(path)) - elem = etree.Element('tagNode') - node_type = TAG_NODE - elif contents == ['node.def']: - print("Creating a leaf node from {0}".format(path)) - elem = etree.Element('leafNode') - node_type = LEAF_NODE - else: - print("Creating a node from {0}".format(path)) - elem = etree.Element('node') - node_type = NODE - - # Read and parse the command definition data (the 'node.def' file) - with open(os.path.join(path, 'node.def'), 'r') as f: - node_def = f.read() - data = parse_command_data(node_def) - - # Import the data into the properties element - props_elem = etree.Element('properties') - - if data['priority']: - # Priority values sometimes come with comments that explain the value choice - try: - prio, prio_comment = re.match(r'\s*(\d+)\s*#(.*)', data['priority']).groups() - except: - prio = data['priority'].strip() - prio_comment = None - prio_elem = etree.Element('priority') - prio_elem.text = prio - props_elem.append(prio_elem) - if prio_comment: - prio_comment_elem = etree.Comment(prio_comment) - props_elem.append(prio_comment_elem) - - if data['multi']: - multi_elem = etree.Element('multi') - props_elem.append(multi_elem) - - if data['help']: - help_elem = etree.Element('help') - help_elem.text = data['help'] - props_elem.append(help_elem) - - # For leaf nodes, absense of a type: tag means they take no values - # For any other nodes, it doesn't mean anything - if not data['type'] and (node_type == LEAF_NODE): - valueless = etree.Element('valueless') - props_elem.append(valueless) - - # There can be only one constraint element in the definition - # Create it now, we'll modify it in the next two cases, then append - constraint_elem = etree.Element('constraint') - has_constraint = False - - # Add regexp field for multiple options - if data['syntax_expression_var']: - regex = etree.Element('regex') - constraint_error=etree.Element('constraintErrorMessage') - values = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(1) - message = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(2) - values = re.findall(r'\"(.+?)\"', values) - regex.text = '|'.join(values) - constraint_error.text = re.sub('\".*?VAR.*?\"', '', message) - constraint_error.text = re.sub(r'[\"|\\]', '', message) - constraint_elem.append(regex) - props_elem.append(constraint_elem) - props_elem.append(constraint_error) - - if data['val_help']: - for vh in data['val_help']: - vh_elem = etree.Element('valueHelp') - - vh_fmt_elem = etree.Element('format') - # Many commands use special "u32:<start>-<end>" format for ranges - if re.match(r'u32:', vh[0]): - vh_fmt = re.match(r'u32:(.*)', vh[0]).group(1).strip() - - # If valid range of values is specified in val_help, we can automatically - # create a constraint for it - # Extracting it from syntax:expression: would be much more complicated - vh_validator = etree.Element('validator') - vh_validator.set("name", "numeric") - vh_validator.set("argument", "--range {0}".format(vh_fmt)) - constraint_elem.append(vh_validator) - has_constraint = True - else: - vh_fmt = vh[0] - vh_fmt_elem.text = vh_fmt - - vh_help_elem = etree.Element('description') - vh_help_elem.text = vh[1] - - vh_elem.append(vh_fmt_elem) - vh_elem.append(vh_help_elem) - props_elem.append(vh_elem) - - # Translate the "type:" to the new validator system - if data['type']: - t = data['type'] - if t == 'txt': - # Can't infer anything from the generic "txt" type - pass - else: - validator = etree.Element('validator') - if t == 'u32': - validator.set('name', 'numeric') - validator.set('argument', '--non-negative') - elif t == 'ipv4': - validator.set('name', 'ipv4-address') - elif t == 'ipv4net': - validator.set('name', 'ipv4-prefix') - elif t == 'ipv6': - validator.set('name', 'ipv6-address') - elif t == 'ipv6net': - validator.set('name', 'ipv6-prefix') - elif t == 'macaddr': - validator.set('name', 'mac-address') - else: - print("Warning: unsupported type \'{0}\'".format(t)) - validator = None - - if (validator is not None) and (not has_constraint): - # If has_constraint is true, it means a more specific validator - # was already extracted from another option - constraint_elem.append(validator) - has_constraint = True - - if has_constraint: - props_elem.append(constraint_elem) - - elem.append(props_elem) - - elem.set("name", name) - - if node_type != LEAF_NODE: - children = etree.Element('children') - - # Create the next level dir path, - # accounting for the "virtual" node.tag subdir for tag nodes - next_level = path - if node_type == TAG_NODE: - next_level = os.path.join(path, 'node.tag') - - # Walk the subdirs of the next level - for d in os.listdir(next_level): - dp = os.path.join(next_level, d) - if os.path.isdir(dp): - walk(children, next_level, d) - - elem.append(children) - - tree.append(elem) - -if __name__ == '__main__': - if len(sys.argv) < 2: - print("Usage: {0} <base path>".format(sys.argv[0])) - sys.exit(1) - else: - base_path = sys.argv[1] - - root = etree.Element('interfaceDefinition') - contents = os.listdir(base_path) - elem = etree.Element('node') - elem.set('name', os.path.basename(base_path)) - children = etree.Element('children') - - for c in contents: - path = os.path.join(base_path, c) - if os.path.isdir(path): - walk(children, base_path, c) - - elem.append(children) - root.append(elem) - - xml_data = etree.tostring(root, pretty_print=True).decode() - with open('output.xml', 'w') as f: - f.write(xml_data) diff --git a/smoketest/configs/cluster-basic b/smoketest/configs/cluster-basic new file mode 100644 index 000000000..1e34999c1 --- /dev/null +++ b/smoketest/configs/cluster-basic @@ -0,0 +1,62 @@ +cluster { + dead-interval 500 + group VyOS { + auto-failback true + primary vyos1 + secondary vyos2 + service 192.0.2.10/24/eth1 + service 192.0.2.20/24 + } + interface eth1 + keepalive-interval 100 + monitor-dead-interval 420 + pre-shared-secret qwerty +} +interfaces { + ethernet eth0 { + duplex auto + smp-affinity auto + speed auto + } + ethernet eth1 { + address 192.0.2.1/24 + duplex auto + smp-affinity auto + speed auto + } + loopback lo { + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } + time-zone Antarctica/South_Pole +} +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.3 diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 7b4ba11d0..0de4354d2 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -226,6 +226,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): 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', 'enable-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']) @@ -248,7 +249,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): 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', 'accept']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'enable-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', 'interface-name', interface_inv]) @@ -262,21 +264,24 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['chain VYOS_FORWARD_filter'], - ['type filter hook forward priority filter; policy drop;'], + ['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 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 "[smoketest-default-D]"','smoketest default-action', 'drop'] + ['log prefix "[ipv4-smoketest-default-D]"','smoketest default-action', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -326,16 +331,18 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['chain VYOS_FORWARD_filter'], - ['type filter hook forward priority filter; policy drop;'], + ['type filter hook forward priority filter; policy accept;'], ['ip saddr 198.51.100.1', 'meta mark 0x000003f2', 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 "[{name}-default-D]"', 'drop'] + [f'log prefix "[ipv4-{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -411,12 +418,14 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): 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', 'enable-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', 'interface-name', interface]) self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'enable-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', 'interface-name', interface]) @@ -432,15 +441,18 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['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 drop;'], + ['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'], [f'chain NAME6_{name}'], ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'], - [f'"{name} default-action drop"', f'log prefix "[{name}-default-D]"', 'drop'] + [f'"{name} default-action drop"', f'log prefix "[ipv6-{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') @@ -483,7 +495,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['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 "[{name}-default-D]"', 'drop'] + [f'log prefix "[ipv6-{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') @@ -635,6 +647,44 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): with open(path, 'r') as f: self.assertNotEqual(f.read().strip(), conf['default'], msg=path) +### Zone + def test_zone_basic(self): + self.cli_set(['firewall', 'ipv4', '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 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'] + ] + + 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) + + def test_flow_offload(self): self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware']) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 967958cab..5e3402fa8 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -340,6 +340,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # AFI maximum path support self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ebgp', max_path_v4]) self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp]) + self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ebgp', max_path_v4]) + self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp]) self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ebgp', max_path_v6]) self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ibgp', max_path_v6ibgp]) @@ -373,6 +375,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) + afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) + self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) + afiv6_config = self.getFRRconfig(' address-family ipv6 unicast') self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config) self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f6480ab0a..9791cf009 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -374,12 +374,82 @@ def verify(firewall): for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, True) + #### ZONESSSS + 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, 'ipv4', '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, 'ipv4', '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 def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True + 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'] = {} + + 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/migration-scripts/cluster/1-to-2 b/src/migration-scripts/cluster/1-to-2 new file mode 100755 index 000000000..a2e589155 --- /dev/null +++ b/src/migration-scripts/cluster/1-to-2 @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 re +import sys + +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + + file_name = sys.argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + if not config.exists(['cluster']): + # Cluster is not set -- nothing to do at all + sys.exit(0) + + # If at least one cluster group is defined, we have real work to do. + # If there are no groups, we remove the top-level cluster node at the end of this script anyway. + if config.exists(['cluster', 'group']): + # First, gather timer and interface settings to duplicate them in all groups, + # since in the old cluster they are global, but in VRRP they are always per-group + + global_interface = None + if config.exists(['cluster', 'interface']): + global_interface = config.return_value(['cluster', 'interface']) + else: + # Such configs shouldn't exist in practice because interface is a required option. + # But since it's possible to specify interface inside 'service' options, + # we may be able to convert such configs nonetheless. + print("Warning: incorrect cluster config: interface is not defined.", file=sys.stderr) + + # There are three timers: advertise-interval, dead-interval, and monitor-dead-interval + # Only the first one makes sense for the VRRP, we translate it to advertise-interval + advertise_interval = None + if config.exists(['cluster', 'keepalive-interval']): + advertise_interval = config.return_value(['cluster', 'keepalive-interval']) + + if advertise_interval is not None: + # Cluster had all timers in milliseconds, so we need to convert them to seconds + # And ensure they are not shorter than one second + advertise_interval = int(advertise_interval) // 1000 + if advertise_interval < 1: + advertise_interval = 1 + + # Cluster had password as a global option, in VRRP it's per-group + password = None + if config.exists(['cluster', 'pre-shared-secret']): + password = config.return_value(['cluster', 'pre-shared-secret']) + + # Set up the stage for converting cluster groups to VRRP groups + free_vrids = set(range(1,255)) + vrrp_base_path = ['high-availability', 'vrrp', 'group'] + if not config.exists(vrrp_base_path): + # If VRRP is not set up, create a node and set it to 'tag node' + # Setting it to 'tag' is not mandatory but it's better to be consistent + # with configs produced by 'save' + config.set(vrrp_base_path) + config.set_tag(vrrp_base_path) + else: + # If there are VRRP groups already, we need to find the set of unused VRID numbers to avoid conflicts + existing_vrids = set() + for vg in config.list_nodes(vrrp_base_path): + existing_vrids.add(int(config.return_value(vrrp_base_path + [vg, 'vrid']))) + free_vrids = free_vrids.difference(existing_vrids) + + # Now handle cluster groups + groups = config.list_nodes(['cluster', 'group']) + for g in groups: + base_path = ['cluster', 'group', g] + service_names = config.return_values(base_path + ['service']) + + # Cluster used to allow services other than IP addresses, at least nominally + # Whether that ever worked is a big question, but we need to consider that, + # since configs with custom services are definitely impossible to meaningfully migrate now + services = {"ip": [], "other": []} + for s in service_names: + if re.match(r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2})(/[a-z]+\d+)?$', s): + services["ip"].append(s) + else: + services["other"].append(s) + + if services["other"]: + print("Cluster config includes non-IP address services and cannot be migrated", file=sys.stderr) + sys.exit(1) + + # Cluster allowed virtual IPs for different interfaces within a single group. + # VRRP groups are by definition bound to interfaces, so we cannot migrate such configurations. + # Thus we need to find out if all addresses either leave the interface unspecified + # (in that case the global 'cluster interface' option is used), + # or have the same interface, or have the same interface as the global 'cluster interface'. + + # First, we collect all addresses and check if they have interface specified + # If not, we substitute the global interface option + # or throw an error if it's not in the config. + ips = [] + for ip in services["ip"]: + ip_with_intf = re.match(r'^(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})/(?P<intf>[a-z]+\d+)$', ip) + if ip_with_intf: + ips.append({"ip": ip_with_intf.group("ip"), "interface": ip_with_intf.group("intf")}) + else: + if global_interface is not None: + ips.append({"ip": ip, "interface": global_interface}) + else: + print("Error: cluster has groups with IPs without interfaces and 'cluster interface' is not specified.", file=sys.stderr) + sys.exit(1) + + # Then we check if all addresses are for the same interface. + intfs_set = set(map(lambda i: i["interface"], ips)) + if len(intfs_set) > 1: + print("Error: cluster group has addresses for different interfaces", file=sys.stderr) + sys.exit(1) + + # If we got this far, the group is migratable. + + # Extract the interface from the set -- we know there's only a single member. + interface = intfs_set.pop() + + addresses = list(map(lambda i: i["ip"], ips)) + vrrp_path = ['high-availability', 'vrrp', 'group', g] + + # If there's already a VRRP group with exactly the same name, + # we probably shouldn't try to make up a unique name, just leave migration to the user... + if config.exists(vrrp_path): + print("Error: VRRP group with the same name as a cluster group already exists", file=sys.stderr) + sys.exit(1) + + config.set(vrrp_path + ['interface'], value=interface) + for a in addresses: + config.set(vrrp_path + ['virtual-address'], value=a, replace=False) + + # Take the next free VRID and assign it to the group + vrid = free_vrids.pop() + config.set(vrrp_path + ['vrid'], value=vrid) + + # Convert the monitor option to VRRP ping health check + if config.exists(base_path + ['monitor']): + monitor_ip = config.return_value(base_path + ['monitor']) + config.set(vrrp_path + ['health-check', 'ping'], value=monitor_ip) + + # Convert "auto-failback" to "no-preempt", if necessary + if config.exists(base_path + ['auto-failback']): + # It's a boolean node that requires "true" or "false" + # so if it exists we still need to check its value + auto_failback = config.return_value(base_path + ['auto-failback']) + if auto_failback == "false": + config.set(vrrp_path + ['no-preempt']) + else: + # It's "true" or we assume it is, which means preemption is desired, + # and in VRRP config it's the default + pass + else: + # The old default for that option is false + config.set(vrrp_path + ['no-preempt']) + + # Inject settings from the global cluster config that have to be per-group in VRRP + if advertise_interval is not None: + config.set(vrrp_path + ['advertise-interval'], value=advertise_interval) + + if password is not None: + config.set(vrrp_path + ['authentication', 'password'], value=password) + config.set(vrrp_path + ['authentication', 'type'], value='plaintext-password') + + # Finally, clean up the old cluster node + config.delete(['cluster']) + + 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)) + sys.exit(1) diff --git a/src/migration-scripts/firewall/10-to-11 b/src/migration-scripts/firewall/10-to-11 index 716c5a240..b739fb139 100755 --- a/src/migration-scripts/firewall/10-to-11 +++ b/src/migration-scripts/firewall/10-to-11 @@ -181,191 +181,6 @@ if config.exists(base + ['interface']): config.delete(base + ['interface']) - -### Migration of zones: -### User interface groups -if config.exists(base + ['zone']): - inp_ipv4_rule = 101 - inp_ipv6_rule = 101 - fwd_ipv4_rule = 101 - fwd_ipv6_rule = 101 - out_ipv4_rule = 101 - out_ipv6_rule = 101 - local_zone = 'False' - - for zone in config.list_nodes(base + ['zone']): - if config.exists(base + ['zone', zone, 'local-zone']): - local_zone = 'True' - # Add default-action== accept for compatibility reasons: - config.set(base + ['ipv4', 'input', 'filter', 'default-action'], value='accept') - config.set(base + ['ipv6', 'input', 'filter', 'default-action'], value='accept') - config.set(base + ['ipv4', 'output', 'filter', 'default-action'], value='accept') - config.set(base + ['ipv6', 'output', 'filter', 'default-action'], value='accept') - for from_zone in config.list_nodes(base + ['zone', zone, 'from']): - group_name = 'IG_' + from_zone - if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']): - # ipv4 input ruleset - target_ipv4_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']) - config.set(base + ['ipv4', 'input', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'input', 'filter', 'rule']) - config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'inbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'action'], value='jump') - config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'jump-target'], value=target_ipv4_chain) - inp_ipv4_rule = inp_ipv4_rule + 5 - if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']): - # ipv6 input ruleset - target_ipv6_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']) - config.set(base + ['ipv6', 'input', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'input', 'filter', 'rule']) - config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'inbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'action'], value='jump') - config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'jump-target'], value=target_ipv6_chain) - inp_ipv6_rule = inp_ipv6_rule + 5 - - # Migrate: set firewall zone <zone> default-action <action> - # Options: drop or reject. If not specified, is drop - if config.exists(base + ['zone', zone, 'default-action']): - local_def_action = config.return_value(base + ['zone', zone, 'default-action']) - else: - local_def_action = 'drop' - config.set(base + ['ipv4', 'input', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'input', 'filter', 'rule']) - config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'action'], value=local_def_action) - config.set(base + ['ipv6', 'input', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'input', 'filter', 'rule']) - config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'action'], value=local_def_action) - if config.exists(base + ['zone', zone, 'enable-default-log']): - config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'log'], value='enable') - config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'log'], value='enable') - - else: - # It's not a local zone - group_name = 'IG_' + zone - # Add default-action== accept for compatibility reasons: - config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept') - config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept') - # intra-filtering migration. By default accept - intra_zone_ipv4_action = 'accept' - intra_zone_ipv6_action = 'accept' - - if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'action']): - intra_zone_ipv4_action = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'action']) - intra_zone_ipv6_action = intra_zone_ipv4_action - else: - if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']): - intra_zone_ipv4_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']) - intra_zone_ipv4_action = 'jump' - if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']): - intra_zone_ipv6_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']) - intra_zone_ipv6_action = 'jump' - config.set(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'inbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value=intra_zone_ipv4_action) - config.set(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'inbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value=intra_zone_ipv6_action) - if intra_zone_ipv4_action == 'jump': - if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']): - intra_zone_ipv4_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'jump-target'], value=intra_zone_ipv4_target) - if intra_zone_ipv6_action == 'jump': - if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']): - intra_zone_ipv6_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'jump-target'], value=intra_zone_ipv6_target) - fwd_ipv4_rule = fwd_ipv4_rule + 5 - fwd_ipv6_rule = fwd_ipv6_rule + 5 - - if config.exists(base + ['zone', zone, 'interface']): - # Create interface group IG_<zone> - group_name = 'IG_' + zone - config.set(base + ['group', 'interface-group'], value=group_name) - config.set_tag(base + ['group', 'interface-group']) - for iface in config.return_values(base + ['zone', zone, 'interface']): - config.set(base + ['group', 'interface-group', group_name, 'interface'], value=iface, replace=False) - - if config.exists(base + ['zone', zone, 'from']): - for from_zone in config.list_nodes(base + ['zone', zone, 'from']): - from_group = 'IG_' + from_zone - if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']): - target_ipv4_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']) - if config.exists(base + ['zone', from_zone, 'local-zone']): - # It's from LOCAL zone -> Output filtering - config.set(base + ['ipv4', 'output', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'output', 'filter', 'rule']) - config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'action'], value='jump') - config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'jump-target'], value=target_ipv4_chain) - out_ipv4_rule = out_ipv4_rule + 5 - else: - # It's not LOCAL zone -> forward filtering - config.set(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'inbound-interface', 'interface-group'], value=from_group) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value='jump') - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'jump-target'], value=target_ipv4_chain) - fwd_ipv4_rule = fwd_ipv4_rule + 5 - if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']): - target_ipv6_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']) - if config.exists(base + ['zone', from_zone, 'local-zone']): - # It's from LOCAL zone -> Output filtering - config.set(base + ['ipv6', 'output', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'output', 'filter', 'rule']) - config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'action'], value='jump') - config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'jump-target'], value=target_ipv6_chain) - out_ipv6_rule = out_ipv6_rule + 5 - else: - # It's not LOCAL zone -> forward filtering - config.set(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'inbound-interface', 'interface-group'], value=from_group) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value='jump') - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'jump-target'], value=target_ipv6_chain) - fwd_ipv6_rule = fwd_ipv6_rule + 5 - - ## Now need to migrate: set firewall zone <zone> default-action <action> # action=drop if not specified. - if config.exists(base + ['zone', zone, 'default-action']): - def_action = config.return_value(base + ['zone', zone, 'default-action']) - else: - def_action = 'drop' - config.set(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule']) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value=def_action) - description = 'zone_' + zone + ' default-action' - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'description'], value=description) - config.set(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule']) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value=def_action) - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'description'], value=description) - - if config.exists(base + ['zone', zone, 'enable-default-log']): - config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'log'], value='enable') - config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'log'], value='enable') - fwd_ipv4_rule = fwd_ipv4_rule + 5 - fwd_ipv6_rule = fwd_ipv6_rule + 5 - - # Migrate default-action (force to be drop in output chain) if local zone is defined - if local_zone == 'True': - # General drop in output change if needed - config.set(base + ['ipv4', 'output', 'filter', 'rule']) - config.set_tag(base + ['ipv4', 'output', 'filter', 'rule']) - config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'action'], value=local_def_action) - config.set(base + ['ipv6', 'output', 'filter', 'rule']) - config.set_tag(base + ['ipv6', 'output', 'filter', 'rule']) - config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'action'], value=local_def_action) - - config.delete(base + ['zone']) - -###### END migration zones - try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/op_mode/generate_tech-support_archive.py b/src/op_mode/generate_tech-support_archive.py index 23d81f986..c490b0137 100755 --- a/src/op_mode/generate_tech-support_archive.py +++ b/src/op_mode/generate_tech-support_archive.py @@ -100,7 +100,7 @@ if __name__ == '__main__': location_path = args.path[:-1] if args.path[-1] == '/' else args.path hostname: str = gethostname() - time_now: str = datetime.now().isoformat(timespec='seconds') + time_now: str = datetime.now().isoformat(timespec='seconds').replace(":", "-") remote = False tmp_path = '' @@ -145,4 +145,4 @@ if __name__ == '__main__': rmtree(tmp_dir) finally: # cleanup - exit()
\ No newline at end of file + exit() |