diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 1c70a6b77..10cbc68cb 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -1,225 +1,225 @@ #!/usr/sbin/nft -f {% import 'firewall/nftables-defines.j2' as group_tmpl %} {% if first_install is not vyos_defined %} delete table ip vyos_filter {% endif %} table ip vyos_filter { {% if ipv4 is vyos_defined %} {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('FWD', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('INP',prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('OUT', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['OUT_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} chain VYOS_FRAG_MARK { type filter hook prerouting priority -450; policy accept; ip frag-off & 0x3fff != 0 meta mark set 0xffff1 return } {% if ipv4.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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('PRE', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['PRE_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(prior) }} } {% endfor %} {% endif %} {% if ipv4.name is vyos_defined %} {% for name_text, conf in ipv4.name.items() %} chain NAME_{{ name_text }} { {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('NAM', name_text, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(name_text) }} } {% endfor %} {% endif %} {% for set_name in ns.sets %} set RECENT_{{ set_name }} { type ipv4_addr size 65535 flags dynamic } {% endfor %} {% for set_name in ip_fqdn %} set FQDN_{{ set_name }} { type ipv4_addr flags interval } {% endfor %} {% if geoip_updated.name is vyos_defined %} {% for setname in geoip_updated.name %} set {{ setname }} { type ipv4_addr flags interval } {% endfor %} {% endif %} {% endif %} {{ group_tmpl.groups(group, False) }} } {% if first_install is not vyos_defined %} delete table ip6 vyos_filter {% endif %} table ip6 vyos_filter { {% if ipv6 is vyos_defined %} {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('FWD', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('INP', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% 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 }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('OUT', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['OUT_ ' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} chain VYOS_FRAG6_MARK { type filter hook prerouting priority -450; policy accept; exthdr frag exists meta mark set 0xffff1 return } -{% if ipv6.ipv6_name is vyos_defined %} -{% for name_text, conf in ipv6.ipv6_name.items() %} +{% if ipv6.name is vyos_defined %} +{% for name_text, conf in ipv6.name.items() %} chain NAME6_{{ name_text }} { {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(name_text, ipv6=True) }} } {% endfor %} {% endif %} {% for set_name in ns.sets %} set RECENT6_{{ set_name }} { type ipv6_addr size 65535 flags dynamic } {% endfor %} {% for set_name in ip6_fqdn %} set FQDN_{{ set_name }} { type ipv6_addr flags interval } {% endfor %} {% if geoip_updated.ipv6_name is vyos_defined %} {% for setname in geoip_updated.ipv6_name %} set {{ setname }} { type ipv6_addr flags interval } {% endfor %} {% endif %} {% endif %} {{ group_tmpl.groups(group, True) }} } \ No newline at end of file diff --git a/interface-definitions/include/firewall/ipv6-custom-name.xml.i b/interface-definitions/include/firewall/ipv6-custom-name.xml.i index 6275036c1..4b6777293 100644 --- a/interface-definitions/include/firewall/ipv6-custom-name.xml.i +++ b/interface-definitions/include/firewall/ipv6-custom-name.xml.i @@ -1,49 +1,49 @@ <!-- include start from firewall/ipv6-custom-name.xml.i --> -<tagNode name="ipv6-name"> +<tagNode name="name"> <properties> <help>IPv6 custom firewall</help> <constraint> <regex>[a-zA-Z0-9][\w\-\.]*</regex> </constraint> </properties> <children> #include <include/firewall/default-action.xml.i> #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <leafNode name="default-jump-target"> <properties> <help>Set jump target. Action jump must be defined in default-action to use this setting</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> </leafNode> <tagNode name="rule"> <properties> <help>IPv6 Firewall custom rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number for this firewall rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> #include <include/firewall/outbound-interface.xml.i> <leafNode name="jump-target"> <properties> <help>Set jump target. Action jump must be defined to use this setting</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> </leafNode> </children> </tagNode> </children> </tagNode> <!-- include end --> \ No newline at end of file diff --git a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i index 042bd9931..25e1bd288 100644 --- a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i @@ -1,44 +1,44 @@ <!-- include start from firewall/ipv6-hook-forward.xml.i --> <node name="forward"> <properties> <help>IPv6 forward firewall</help> </properties> <children> <node name="filter"> <properties> <help>IPv6 firewall forward filter</help> </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> <help>IPv6 Firewall forward filter rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number for this firewall rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> #include <include/firewall/outbound-interface.xml.i> <leafNode name="jump-target"> <properties> <help>Set jump target. Action jump must be defined to use this setting</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> </leafNode> </children> </tagNode> </children> </node> </children> </node> <!-- include end --> \ No newline at end of file diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i index 8c41e0aca..f9a4d71b4 100644 --- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i @@ -1,43 +1,43 @@ <!-- include start from firewall/ipv6-hook-input.xml.i --> <node name="input"> <properties> <help>IPv6 input firewall</help> </properties> <children> <node name="filter"> <properties> <help>IPv6 firewall input filter</help> </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> <help>IPv6 Firewall input filter rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number for this firewall rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> <leafNode name="jump-target"> <properties> <help>Set jump target. Action jump must be defined to use this setting</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> </leafNode> </children> </tagNode> </children> </node> </children> </node> <!-- include end --> \ No newline at end of file diff --git a/interface-definitions/include/firewall/ipv6-hook-output.xml.i b/interface-definitions/include/firewall/ipv6-hook-output.xml.i index 9b756d870..9bf73a778 100644 --- a/interface-definitions/include/firewall/ipv6-hook-output.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-output.xml.i @@ -1,43 +1,43 @@ <!-- include start from firewall/ipv6-hook-output.xml.i --> <node name="output"> <properties> <help>IPv6 output firewall</help> </properties> <children> <node name="filter"> <properties> <help>IPv6 firewall output filter</help> </properties> <children> #include <include/firewall/default-action-base-chains.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> <help>IPv6 Firewall output filter rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number for this firewall rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/outbound-interface.xml.i> <leafNode name="jump-target"> <properties> <help>Set jump target. Action jump must be defined to use this setting</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> </leafNode> </children> </tagNode> </children> </node> </children> </node> <!-- include end --> \ No newline at end of file diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index b028f0af0..4aa509fe2 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -1,560 +1,559 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-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 csv import gzip import os import re from pathlib import Path from socket import AF_INET from socket import AF_INET6 from socket import getaddrinfo from time import strftime from vyos.remote import download from vyos.template import is_ipv4 from vyos.template import render from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run # Domain Resolver def fqdn_config_parse(firewall): firewall['ip_fqdn'] = {} firewall['ip6_fqdn'] = {} for domain, path in dict_search_recursive(firewall, 'fqdn'): hook_name = path[1] priority = path[2] fw_name = path[2] rule = path[4] suffix = path[5][0] set_name = f'{hook_name}_{priority}_{rule}_{suffix}' if (path[0] == 'ipv4') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'): firewall['ip_fqdn'][set_name] = domain - elif (path[0] == 'ipv6') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'ipv6_name'): - if path[1] == 'ipv6_name': + elif (path[0] == 'ipv6') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'): + if path[1] == 'name': set_name = f'name6_{priority}_{rule}_{suffix}' firewall['ip6_fqdn'][set_name] = domain def fqdn_resolve(fqdn, ipv6=False): try: res = getaddrinfo(fqdn, None, AF_INET6 if ipv6 else AF_INET) return set(item[4][0] for item in res) except: return None # End Domain Resolver def find_nftables_rule(table, chain, rule_matches=[]): # Find rule in table/chain that matches all criteria and return the handle results = cmd(f'sudo nft -a list chain {table} {chain}').split("\n") for line in results: if all(rule_match in line for rule_match in rule_matches): handle_search = re.search('handle (\d+)', line) if handle_search: return handle_search[1] return None def remove_nftables_rule(table, chain, handle): cmd(f'sudo nft delete rule {table} {chain} handle {handle}') # Functions below used by template generation def nft_action(vyos_action): if vyos_action == 'accept': return 'return' return vyos_action def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output = [] def_suffix = '6' if ip_name == 'ip6' else '' if 'state' in rule_conf and rule_conf['state']: states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable']) if states: output.append(f'ct state {{{states}}}') if 'connection_status' in rule_conf and rule_conf['connection_status']: status = rule_conf['connection_status'] if status['nat'] == 'destination': nat_status = '{dnat}' output.append(f'ct status {nat_status}') if status['nat'] == 'source': nat_status = '{snat}' output.append(f'ct status {nat_status}') if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': proto = rule_conf['protocol'] operator = '' if proto[0] == '!': operator = '!=' proto = proto[1:] if proto == 'tcp_udp': proto = '{tcp, udp}' output.append(f'meta l4proto {operator} {proto}') for side in ['destination', 'source']: if side in rule_conf: prefix = side[0] side_conf = rule_conf[side] address_mask = side_conf.get('address_mask', None) if 'address' in side_conf: suffix = side_conf['address'] operator = '' exclude = suffix[0] == '!' if exclude: operator = '!= ' suffix = suffix[1:] if address_mask: operator = '!=' if exclude else '==' operator = f'& {address_mask} {operator} ' output.append(f'{ip_name} {prefix}addr {operator}{suffix}') if 'fqdn' in side_conf: fqdn = side_conf['fqdn'] hook_name = '' operator = '' if fqdn[0] == '!': operator = '!=' if hook == 'FWD': hook_name = 'forward' if hook == 'INP': hook_name = 'input' if hook == 'OUT': hook_name = 'output' if hook == 'NAM': hook_name = f'name{def_suffix}' output.append(f'{ip_name} {prefix}addr {operator} @FQDN_{hook_name}_{fw_name}_{rule_id}_{prefix}') if dict_search_args(side_conf, 'geoip', 'country_code'): operator = '' hook_name = '' if dict_search_args(side_conf, 'geoip', 'inverse_match') != None: operator = '!=' if hook == 'FWD': hook_name = 'forward' if hook == 'INP': hook_name = 'input' if hook == 'OUT': hook_name = 'output' if hook == 'NAM': - hook_name = f'name{def_suffix}' - output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC_{hook_name}_{fw_name}_{rule_id}') + hook_name = f'name' + output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC{def_suffix}_{hook_name}_{fw_name}_{rule_id}') if 'mac_address' in side_conf: suffix = side_conf["mac_address"] if suffix[0] == '!': suffix = f'!= {suffix[1:]}' output.append(f'ether {prefix}addr {suffix}') if 'port' in side_conf: proto = rule_conf['protocol'] port = side_conf['port'].split(',') ports = [] negated_ports = [] for p in port: if p[0] == '!': negated_ports.append(p[1:]) else: ports.append(p) if proto == 'tcp_udp': proto = 'th' if ports: ports_str = ','.join(ports) output.append(f'{proto} {prefix}port {{{ports_str}}}') if negated_ports: negated_ports_str = ','.join(negated_ports) output.append(f'{proto} {prefix}port != {{{negated_ports_str}}}') if 'group' in side_conf: group = side_conf['group'] if 'address_group' in group: group_name = group['address_group'] operator = '' exclude = group_name[0] == "!" if exclude: operator = '!=' group_name = group_name[1:] if address_mask: operator = '!=' if exclude else '==' operator = f'& {address_mask} {operator}' output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group: group_name = group['domain_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}') elif 'network_group' in group: group_name = group['network_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_name} {prefix}addr {operator} @N{def_suffix}_{group_name}') if 'mac_group' in group: group_name = group['mac_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'ether {prefix}addr {operator} @M_{group_name}') if 'port_group' in group: proto = rule_conf['protocol'] group_name = group['port_group'] if proto == 'tcp_udp': proto = 'th' operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{proto} {prefix}port {operator} @P_{group_name}') if 'log' in rule_conf and rule_conf['log'] == 'enable': action = rule_conf['action'] if 'action' in rule_conf else 'accept' output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') if 'log_options' in rule_conf: if 'level' in rule_conf['log_options']: log_level = rule_conf['log_options']['level'] output.append(f'log level {log_level}') if 'group' in rule_conf['log_options']: log_group = rule_conf['log_options']['group'] output.append(f'log group {log_group}') if 'queue_threshold' in rule_conf['log_options']: queue_threshold = rule_conf['log_options']['queue_threshold'] output.append(f'queue-threshold {queue_threshold}') if 'snapshot_length' in rule_conf['log_options']: log_snaplen = rule_conf['log_options']['snapshot_length'] output.append(f'snaplen {log_snaplen}') if 'hop_limit' in rule_conf: operators = {'eq': '==', 'gt': '>', 'lt': '<'} for op, operator in operators.items(): if op in rule_conf['hop_limit']: value = rule_conf['hop_limit'][op] output.append(f'ip6 hoplimit {operator} {value}') if 'inbound_interface' in rule_conf: if 'interface_name' in rule_conf['inbound_interface']: iiface = rule_conf['inbound_interface']['interface_name'] output.append(f'iifname {{{iiface}}}') else: iiface = rule_conf['inbound_interface']['interface_group'] output.append(f'iifname @I_{iiface}') if 'outbound_interface' in rule_conf: if 'interface_name' in rule_conf['outbound_interface']: oiface = rule_conf['outbound_interface']['interface_name'] output.append(f'oifname {{{oiface}}}') else: oiface = rule_conf['outbound_interface']['interface_group'] output.append(f'oifname @I_{oiface}') if 'ttl' in rule_conf: operators = {'eq': '==', 'gt': '>', 'lt': '<'} for op, operator in operators.items(): if op in rule_conf['ttl']: value = rule_conf['ttl'][op] output.append(f'ip ttl {operator} {value}') for icmp in ['icmp', 'icmpv6']: if icmp in rule_conf: if 'type_name' in rule_conf[icmp]: output.append(icmp + ' type ' + rule_conf[icmp]['type_name']) else: if 'code' in rule_conf[icmp]: output.append(icmp + ' code ' + rule_conf[icmp]['code']) if 'type' in rule_conf[icmp]: output.append(icmp + ' type ' + rule_conf[icmp]['type']) if 'packet_length' in rule_conf: lengths_str = ','.join(rule_conf['packet_length']) output.append(f'ip{def_suffix} length {{{lengths_str}}}') if 'packet_length_exclude' in rule_conf: negated_lengths_str = ','.join(rule_conf['packet_length_exclude']) output.append(f'ip{def_suffix} length != {{{negated_lengths_str}}}') if 'packet_type' in rule_conf: output.append(f'pkttype ' + rule_conf['packet_type']) if 'dscp' in rule_conf: dscp_str = ','.join(rule_conf['dscp']) output.append(f'ip{def_suffix} dscp {{{dscp_str}}}') if 'dscp_exclude' in rule_conf: negated_dscp_str = ','.join(rule_conf['dscp_exclude']) output.append(f'ip{def_suffix} dscp != {{{negated_dscp_str}}}') if 'ipsec' in rule_conf: if 'match_ipsec' in rule_conf['ipsec']: output.append('meta ipsec == 1') if 'match_none' in rule_conf['ipsec']: output.append('meta ipsec == 0') if 'fragment' in rule_conf: # Checking for fragmentation after priority -400 is not possible, # so we use a priority -450 hook to set a mark if 'match_frag' in rule_conf['fragment']: output.append('meta mark 0xffff1') if 'match_non_frag' in rule_conf['fragment']: output.append('meta mark != 0xffff1') if 'limit' in rule_conf: if 'rate' in rule_conf['limit']: output.append(f'limit rate {rule_conf["limit"]["rate"]}') if 'burst' in rule_conf['limit']: output.append(f'burst {rule_conf["limit"]["burst"]} packets') if 'recent' in rule_conf: count = rule_conf['recent']['count'] time = rule_conf['recent']['time'] output.append(f'add @RECENT{def_suffix}_{hook}_{fw_name}_{rule_id} {{ {ip_name} saddr limit rate over {count}/{time} burst {count} packets }}') if 'time' in rule_conf: output.append(parse_time(rule_conf['time'])) tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: output.append(parse_tcp_flags(tcp_flags)) # TCP MSS tcp_mss = dict_search_args(rule_conf, 'tcp', 'mss') if tcp_mss: output.append(f'tcp option maxseg size {tcp_mss}') if 'connection_mark' in rule_conf: conn_mark_str = ','.join(rule_conf['connection_mark']) output.append(f'ct mark {{{conn_mark_str}}}') output.append('counter') if 'set' in rule_conf: output.append(parse_policy_set(rule_conf['set'], def_suffix)) if 'action' in rule_conf: # Change action=return to action=action # #output.append(nft_action(rule_conf['action'])) output.append(f'{rule_conf["action"]}') if 'jump' in rule_conf['action']: target = rule_conf['jump_target'] output.append(f'NAME{def_suffix}_{target}') if 'queue' in rule_conf['action']: if 'queue' in rule_conf: target = rule_conf['queue'] output.append(f'num {target}') if 'queue_options' in rule_conf: queue_opts = ','.join(rule_conf['queue_options']) output.append(f'{queue_opts}') else: output.append('return') output.append(f'comment "{hook}-{fw_name}-{rule_id}"') return " ".join(output) def parse_tcp_flags(flags): include = [flag for flag in flags if flag != 'not'] exclude = list(flags['not']) if 'not' in flags else [] return f'tcp flags & ({"|".join(include + exclude)}) == {"|".join(include) if include else "0x0"}' def parse_time(time): out = [] if 'startdate' in time: start = time['startdate'] if 'T' not in start and 'starttime' in time: start += f' {time["starttime"]}' out.append(f'time >= "{start}"') if 'starttime' in time and 'startdate' not in time: out.append(f'hour >= "{time["starttime"]}"') if 'stopdate' in time: stop = time['stopdate'] if 'T' not in stop and 'stoptime' in time: stop += f' {time["stoptime"]}' out.append(f'time < "{stop}"') if 'stoptime' in time and 'stopdate' not in time: out.append(f'hour < "{time["stoptime"]}"') if 'weekdays' in time: days = time['weekdays'].split(",") out_days = [f'"{day}"' for day in days if day[0] != '!'] out.append(f'day {{{",".join(out_days)}}}') return " ".join(out) def parse_policy_set(set_conf, def_suffix): out = [] if 'connection_mark' in set_conf: conn_mark = set_conf['connection_mark'] out.append(f'ct mark set {conn_mark}') if 'dscp' in set_conf: dscp = set_conf['dscp'] out.append(f'ip{def_suffix} dscp set {dscp}') if 'mark' in set_conf: mark = set_conf['mark'] out.append(f'meta mark set {mark}') if 'table' in set_conf: table = set_conf['table'] if table == 'main': table = '254' mark = 0x7FFFFFFF - int(table) out.append(f'meta mark set {mark}') if 'tcp_mss' in set_conf: mss = set_conf['tcp_mss'] out.append(f'tcp option maxseg size set {mss}') return " ".join(out) # GeoIP nftables_geoip_conf = '/run/nftables-geoip.conf' geoip_database = '/usr/share/vyos-geoip/dbip-country-lite.csv.gz' geoip_lock_file = '/run/vyos-geoip.lock' def geoip_load_data(codes=[]): data = None if not os.path.exists(geoip_database): return [] try: with gzip.open(geoip_database, mode='rt') as csv_fh: reader = csv.reader(csv_fh) out = [] for start, end, code in reader: if code.lower() in codes: out.append([start, end, code.lower()]) return out except: print('Error: Failed to open GeoIP database') return [] def geoip_download_data(): url = 'https://download.db-ip.com/free/dbip-country-lite-{}.csv.gz'.format(strftime("%Y-%m")) try: dirname = os.path.dirname(geoip_database) if not os.path.exists(dirname): os.mkdir(dirname) download(geoip_database, url) print("Downloaded GeoIP database") return True except: print("Error: Failed to download GeoIP database") return False class GeoIPLock(object): def __init__(self, file): self.file = file def __enter__(self): if os.path.exists(self.file): return False Path(self.file).touch() return True def __exit__(self, exc_type, exc_value, tb): os.unlink(self.file) def geoip_update(firewall, force=False): with GeoIPLock(geoip_lock_file) as lock: if not lock: print("Script is already running") return False if not firewall: print("Firewall is not configured") return True if not os.path.exists(geoip_database): if not geoip_download_data(): return False elif force: geoip_download_data() ipv4_codes = {} ipv6_codes = {} ipv4_sets = {} ipv6_sets = {} # Map country codes to set names for codes, path in dict_search_recursive(firewall, 'country_code'): set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' - if path[1] == 'ipv6_name': - set_name = f'GEOIP_CC_name6_{path[2]}_{path[4]}' - if ( path[0] == 'ipv4' ) and ( path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name' ): + if ( path[0] == 'ipv4'): for code in codes: ipv4_codes.setdefault(code, []).append(set_name) - elif ( path[0] == 'ipv6' ) and ( path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'ipv6_name' ): + elif ( path[0] == 'ipv6' ): + set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' for code in codes: ipv6_codes.setdefault(code, []).append(set_name) if not ipv4_codes and not ipv6_codes: if force: print("GeoIP not in use by firewall") return True geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes]) # Iterate IP blocks to assign to sets for start, end, code in geoip_data: ipv4 = is_ipv4(start) if code in ipv4_codes and ipv4: ip_range = f'{start}-{end}' if start != end else start for setname in ipv4_codes[code]: ipv4_sets.setdefault(setname, []).append(ip_range) if code in ipv6_codes and not ipv4: ip_range = f'{start}-{end}' if start != end else start for setname in ipv6_codes[code]: ipv6_sets.setdefault(setname, []).append(ip_range) render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', { 'ipv4_sets': ipv4_sets, 'ipv6_sets': ipv6_sets }) result = run(f'nft -f {nftables_geoip_conf}') if result != 0: print('Error: GeoIP failed to update firewall') return False return True diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index bd7666313..9412ce984 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -1,531 +1,531 @@ #!/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 glob import glob from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import run sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'}, 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'}, 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'}, 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'}, 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'} } class TestFirewall(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestFirewall, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, ['firewall']) @classmethod def tearDownClass(cls): super(TestFirewall, cls).tearDownClass() def tearDown(self): self.cli_delete(['firewall']) self.cli_commit() # Verify chains/sets are cleaned up from nftables nftables_search = [ ['set M_smoketest_mac'], ['set N_smoketest_network'], ['set P_smoketest_port'], ['set D_smoketest_domain'], ['set RECENT_smoketest_4'], ['chain NAME_smoketest'] ] self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True) def verify_nftables(self, nftables_search, table, inverse=False, args=''): nftables_output = cmd(f'sudo nft {args} list table {table}') 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(not matched if inverse else matched, msg=search) def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): # Resolver no longer blocks commit, need to wait for daemon to populate set count = 0 while count < max_wait: code = run(f'sudo nft get element {table} {set_name} {{ {element} }}') if code == 0: return True count += 1 sleep(1) return False def test_geoip(self): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) self.cli_commit() nftables_search = [ ['ip saddr @GEOIP_CC_name_smoketest_1', 'drop'], ['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept'] ] # -t prevents 1000+ GeoIP elements being returned self.verify_nftables(nftables_search, 'ip vyos_filter', args='-t') def test_groups(self): hostmap_path = ['system', 'static-host-mapping', 'host-name'] example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11'] self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5']) for ips in example_org: self.cli_set(hostmap_path + ['example.org', 'inet', ips]) self.cli_commit() self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'interface-group', 'smoketest_interface']) self.cli_commit() self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') nftables_search = [ ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'accept'], ['elements = { 172.16.99.0/24 }'], ['elements = { 53, 123 }'], ['ether saddr @M_smoketest_mac', 'accept'], ['elements = { 00:01:02:03:04:05 }'], ['set D_smoketest_domain'], ['elements = { 192.0.2.5, 192.0.2.8,'], ['192.0.2.10, 192.0.2.11 }'], ['ip saddr @D_smoketest_domain', 'accept'], ['oifname @I_smoketest_interface', 'accept'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') self.cli_delete(['system', 'static-host-mapping']) self.cli_commit() def test_nested_groups(self): self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'port', '123']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'include', 'smoketest_port']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network1']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port1']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp']) self.cli_commit() # Test circular includes self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) nftables_search = [ ['ip saddr @N_smoketest_network1', 'th dport @P_smoketest_port1', 'accept'], ['elements = { 172.16.99.0/24, 172.16.101.0/24 }'], ['elements = { 53, 123 }'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_basic_rules(self): name = 'smoketest' interface = 'eth0' interface_wc = 'l2tp*' mss_range = '501-1460' conn_mark = '555' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log-options', 'level', 'debug']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'ttl', 'eq', '15']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'destination', 'port', '8888']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log-options', 'level', 'err']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'not', 'ack']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'ttl', 'gt', '102']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'limit', 'rate', '5/minute']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log', 'disable']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'count', '10']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'time', 'minute']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'packet-type', 'host']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark]) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'accept']) 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_wc]) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark]) self.cli_commit() mark_hex = "{0:#010x}".format(int(conn_mark)) nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy drop;'], ['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'], ['chain VYOS_INPUT_filter'], ['type filter hook input priority filter; policy drop;'], ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface}"', 'meta pkttype broadcast', 'accept'], ['meta l4proto gre', f'ct mark {mark_hex}', 'return'], ['chain VYOS_OUTPUT_filter'], ['type filter hook output priority filter; policy accept;'], ['meta l4proto gre', f'oifname "{interface_wc}"', 'drop'], ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'], ['chain NAME_smoketest'], ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'], ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_advanced(self): name = 'smoketest-adv' name2 = 'smoketest-adv2' interface = 'eth0' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '64']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '512']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '1024']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '17']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '52']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'group', '66']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'snapshot-length', '6666']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'queue-threshold','32000']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length', '1-30000']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length-exclude', '60000-65535']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp', '3-11']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp-exclude', '21-25']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'address', '198.51.100.1']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'action', 'queue']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'queue', '3']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'protocol', 'udp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'action', 'queue']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'fanout']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'bypass']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue', '0-15']) self.cli_commit() nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], ['ip saddr 198.51.100.1', f'jump NAME_{name}'], ['chain VYOS_INPUT_filter'], ['type filter hook input priority filter; policy drop;'], [f'meta l4proto tcp','queue to 3'], [f'meta l4proto udp','queue flags bypass,fanout to 0-15'], [f'chain NAME_{name}'], ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[{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'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_mask(self): name = 'smoketest-mask' interface = 'eth0' self.cli_set(['firewall', 'group', 'address-group', 'mask_group', 'address', '1.1.1.1']) self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '0.0.1.2']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address-mask', '0.0.255.255']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address', '!0.0.3.4']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address-mask', '0.0.255.255']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'address-mask', '0.0.255.255']) self.cli_commit() nftables_search = [ [f'daddr & 0.0.255.255 == 0.0.1.2'], [f'saddr & 0.0.255.255 != 0.0.3.4'], [f'saddr & 0.0.255.255 == @A_mask_group'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv6_basic_rules(self): name = 'v6-smoketest' interface = 'eth0' - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'default-action', 'drop']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'enable-default-log']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'action', 'accept']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'source', 'address', '2002::1']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'destination', 'address', '2002::1:1']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'log', 'enable']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'log-options', 'level', 'crit']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'source', 'address', '2002::1']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '2002::1:1']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log', 'enable']) + 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', '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', '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]) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'udp']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'source', 'address', '2002::1:2']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'interface-name', interface]) self.cli_commit() nftables_search = [ ['chain VYOS_IPV6_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], ['meta l4proto { tcp, udp }', 'th dport 8888', f'iifname "{interface}"', 'reject'], ['chain VYOS_IPV6_INPUT_filter'], ['type filter hook input priority filter; policy drop;'], ['meta l4proto udp', 'ip6 saddr 2002::1:2', f'iifname "{interface}"', 'accept'], ['chain VYOS_IPV6_OUTPUT_filter'], ['type filter hook output priority filter; policy drop;'], ['meta l4proto gre', f'oifname "{interface}"', 'return'], [f'chain NAME6_{name}'], ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[v6-smoketest-1-A]" log level crit', 'accept'], [f'"{name} default-action drop"', f'log prefix "[{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv6_advanced(self): name = 'v6-smoketest-adv' name2 = 'v6-smoketest-adv2' interface = 'eth0' - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'default-action', 'drop']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'enable-default-log']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'action', 'accept']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'packet-length', '65']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'packet-length', '513']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'packet-length', '1025']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'dscp', '18']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'dscp', '53']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '65']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '513']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '1025']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '18']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '53']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length', '1-1999']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length-exclude', '60000-65535']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp', '4-14']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp-exclude', '31-35']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'default-action', 'accept']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'source', 'address', '2001:db8::/64']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'jump-target', name]) self.cli_commit() nftables_search = [ ['chain VYOS_IPV6_FORWARD_filter'], ['type filter hook forward priority filter; policy drop;'], ['ip6 length 1-1999', 'ip6 length != 60000-65535', 'ip6 dscp 0x04-0x0e', 'ip6 dscp != 0x1f-0x23', 'accept'], ['chain VYOS_IPV6_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], ['ip6 saddr 2001:db8::/64', 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'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv6_mask(self): name = 'v6-smoketest-mask' interface = 'eth0' self.cli_set(['firewall', 'group', 'ipv6-address-group', 'mask_group', 'address', '::beef']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'default-action', 'drop']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'enable-default-log']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'action', 'drop']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '2', 'action', 'accept']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'action', 'drop']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) - self.cli_set(['firewall', 'ipv6', 'ipv6-name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) self.cli_commit() nftables_search = [ ['daddr & ::ffff:ffff:ffff:ffff == ::1111:2222:3333:4444'], ['saddr & ::ffff:ffff:ffff:ffff != ::aaaa:bbbb:cccc:dddd'], ['saddr & ::ffff:ffff:ffff:ffff == @A6_mask_group'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv4_state_and_status_rules(self): name = 'smoketest-state' interface = 'eth0' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'connection-status', 'nat', 'destination']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'connection-status', 'nat', 'source']) self.cli_commit() nftables_search = [ ['ct state { established, related }', 'accept'], ['ct state invalid', 'reject'], ['ct state new', 'ct status dnat', 'accept'], ['ct state { established, new }', 'ct status snat', 'accept'], ['drop', f'comment "{name} default-action drop"'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_sysfs(self): for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) for path in paths: with open(path, 'r') as f: self.assertEqual(f.read().strip(), conf['default'], msg=path) self.cli_set(['firewall', 'global-options', name.replace("_", "-"), conf['test_value']]) self.cli_commit() for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) for path in paths: with open(path, 'r') as f: self.assertNotEqual(f.read().strip(), conf['default'], msg=path) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index a50ae2ec6..c8b1e27db 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,481 +1,481 @@ #!/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 import re from glob import glob from json import loads from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdiff import get_config_diff, Diff from vyos.configdep import set_dependents, call_dependents # from vyos.configverify import verify_interface_exists from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update from vyos.template import render from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.process import process_named_running from vyos.utils.process import rc_cmd from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() nat_conf_script = 'nat.py' policy_route_conf_script = 'policy-route.py' nftables_conf = '/run/nftables.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'enable': '0', 'disable': '1'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'enable': '0', 'disable': '-1'}, 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians'}, 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects'}, 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects'}, 'source_validation': {'sysfs': '/proc/sys/net/ipv4/conf/*/rp_filter', 'disable': '0', 'strict': '1', 'loose': '2'}, 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'}, 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'} } valid_groups = [ 'address_group', 'domain_group', 'network_group', 'port_group', 'interface_group' ] nested_group_types = [ 'address_group', 'network_group', 'mac_group', 'port_group', 'ipv6_address_group', 'ipv6_network_group' ] snmp_change_type = { 'unknown': 0, 'add': 1, 'delete': 2, 'change': 3 } snmp_event_source = 1 snmp_trap_mib = 'VYATTA-TRAP-MIB' snmp_trap_name = 'mgmtEventTrap' def geoip_updated(conf, firewall): diff = get_config_diff(conf) node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) out = { 'name': [], 'ipv6_name': [], 'deleted_name': [], 'deleted_ipv6_name': [] } updated = False for key, path in dict_search_recursive(firewall, 'geoip'): set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' - if path[1] == 'ipv6_name': - set_name = f'GEOIP_CC_name6_{path[2]}_{path[4]}' - - if (path[0] == 'ipv4') and ( path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name' ): + if (path[0] == 'ipv4'): out['name'].append(set_name) - elif (path[0] == 'ipv6') and ( path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'ipv6_name' ): + elif (path[0] == 'ipv6'): + set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' out['ipv6_name'].append(set_name) + updated = True if 'delete' in node_diff: for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): - set_name = f'GEOIP_CC_{path[2]}_{path[4]}' - if path[1] == 'name': + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' + if (path[0] == 'ipv4'): out['deleted_name'].append(set_name) - elif path[1] == 'ipv6-name': + elif (path[0] == 'ipv6'): + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' out['deleted_ipv6_name'].append(set_name) updated = True if updated: return out return False def get_config(config=None): if config: conf = config else: conf = Config() base = ['firewall'] firewall = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) # We have gathered the dict representation of the CLI, but there are # default options which we need to update into the dictionary retrived. # XXX: T2665: we currently have no nice way for defaults under tag # nodes, thus we load the defaults "by hand" default_values = defaults(base) for family in ['ipv4', 'ipv6']: - for tmp in ['name', 'ipv6_name', 'forward', 'input', 'output', 'prerouting']: + for tmp in ['name', 'forward', 'input', 'output', 'prerouting']: if tmp in default_values[family]: del default_values[family][tmp] firewall = dict_merge(default_values, firewall) # Merge in defaults for IPv4 ruleset if 'name' in firewall['ipv4']: default_values = defaults(base + ['ipv4'] + ['name']) for name in firewall['ipv4']['name']: firewall['ipv4']['name'][name] = dict_merge(default_values, firewall['ipv4']['name'][name]) for hook in ['forward', 'input', 'output', 'prerouting']: if hook in firewall['ipv4']: for priority in ['filter', 'mangle', 'raw']: if priority in firewall['ipv4'][hook]: default_values = defaults(base + ['ipv4'] + [hook] + [priority]) firewall['ipv4'][hook][priority] = dict_merge(default_values, firewall['ipv4'][hook][priority]) # Merge in defaults for IPv6 ruleset - if 'ipv6_name' in firewall['ipv6']: - default_values = defaults(base + ['ipv6'] + ['ipv6-name']) - for ipv6_name in firewall['ipv6']['ipv6_name']: - firewall['ipv6']['ipv6_name'][ipv6_name] = dict_merge(default_values, - firewall['ipv6']['ipv6_name'][ipv6_name]) + if 'name' in firewall['ipv6']: + default_values = defaults(base + ['ipv6'] + ['name']) + for ipv6_name in firewall['ipv6']['name']: + firewall['ipv6']['name'][ipv6_name] = dict_merge(default_values, + firewall['ipv6']['name'][ipv6_name]) for hook in ['forward', 'input', 'output', 'prerouting']: if hook in firewall['ipv6']: for priority in ['filter', 'mangle', 'raw']: if priority in firewall['ipv6'][hook]: default_values = defaults(base + ['ipv6'] + [hook] + [priority]) firewall['ipv6'][hook][priority] = dict_merge(default_values, firewall['ipv6'][hook][priority]) firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) if firewall['group_resync']: # Update nat and policy-route as firewall groups were updated set_dependents('group_resync', conf) #if 'config_trap' in firewall and firewall['config_trap'] == 'enable': if 'config_trap' in firewall and firewall['global_options']['config_trap'] == 'enable': diff = get_config_diff(conf) firewall['trap_diff'] = diff.get_child_nodes_diff_str(base) firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) firewall['geoip_updated'] = geoip_updated(conf, firewall) fqdn_config_parse(firewall) return firewall def verify_rule(firewall, rule_conf, ipv6): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') if 'jump' in rule_conf['action'] and 'jump_target' not in rule_conf: raise ConfigError('Action set to jump, but no jump-target specified') if 'jump_target' in rule_conf: if 'jump' not in rule_conf['action']: raise ConfigError('jump-target defined, but action jump needed and it is not defined') target = rule_conf['jump_target'] if not ipv6: if target not in dict_search_args(firewall, 'ipv4', 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') else: - if target not in dict_search_args(firewall, 'ipv6', 'ipv6_name'): - raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system') + if target not in dict_search_args(firewall, 'ipv6', 'name'): + raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system') if 'queue_options' in rule_conf: if 'queue' not in rule_conf['action']: raise ConfigError('queue-options defined, but action queue needed and it is not defined') if 'fanout' in rule_conf['queue_options'] and ('queue' not in rule_conf or '-' not in rule_conf['queue']): raise ConfigError('queue-options fanout defined, then queue needs to be defined as a range') if 'queue' in rule_conf and 'queue' not in rule_conf['action']: raise ConfigError('queue defined, but action queue needed and it is not defined') if 'fragment' in rule_conf: if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']): raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') if 'limit' in rule_conf: if 'rate' in rule_conf['limit']: rate_int = re.sub(r'\D', '', rule_conf['limit']['rate']) if int(rate_int) < 1: raise ConfigError('Limit rate integer cannot be less than 1') if 'ipsec' in rule_conf: if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']): raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') if 'recent' in rule_conf: if not {'count', 'time'} <= set(rule_conf['recent']): raise ConfigError('Recent "count" and "time" values must be defined') tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: if dict_search_args(rule_conf, 'protocol') != 'tcp': raise ConfigError('Protocol must be tcp when specifying tcp flags') not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') if not_flags: duplicates = [flag for flag in tcp_flags if flag in not_flags] if duplicates: raise ConfigError(f'Cannot match a tcp flag as set and not set') if 'protocol' in rule_conf: if rule_conf['protocol'] == 'icmp' and ipv6: raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp') if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6: raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp') for side in ['destination', 'source']: if side in rule_conf: side_conf = rule_conf[side] if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1: raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: raise ConfigError('Only one address-group, network-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group error_group = fw_group.replace("_", "-") if group in ['address_group', 'network_group', 'domain_group']: types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') if group_name and group_name[0] == '!': group_name = group_name[1:] group_obj = dict_search_args(firewall, 'group', fw_group, group_name) if group_obj is None: raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): if 'protocol' not in rule_conf: raise ConfigError('Protocol must be defined if specifying a port or port-group') if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'): raise ConfigError(f'{side} port-group and port cannot both be defined') if 'log_options' in rule_conf: if 'log' not in rule_conf or 'enable' not in rule_conf['log']: raise ConfigError('log-options defined, but log is not enable') if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: raise ConfigError('log-options snapshot-length defined, but log group is not define') if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: raise ConfigError('log-options queue-threshold defined, but log group is not define') def verify_nested_group(group_name, group, groups, seen): if 'include' not in group: return seen.append(group_name) for g in group['include']: if g not in groups: raise ConfigError(f'Nested group "{g}" does not exist') if g in seen: raise ConfigError(f'Group "{group_name}" has a circular reference') if 'include' in groups[g]: verify_nested_group(g, groups[g], groups, seen) def verify(firewall): if 'config_trap' in firewall and firewall['config_trap'] == 'enable': if not firewall['trap_targets']: raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') if 'group' in firewall: for group_type in nested_group_types: if group_type in firewall['group']: groups = firewall['group'][group_type] for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) if 'ipv4' in firewall: for name in ['name','forward','input','output']: if name in firewall['ipv4']: for name_id, name_conf in firewall['ipv4'][name].items(): if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: raise ConfigError('default-action set to jump, but no default-jump-target specified') if 'default_jump_target' in name_conf: target = name_conf['default_jump_target'] if 'jump' not in name_conf['default_action']: raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') ## Now need to check that default-jump-target exists (other firewall chain/name) if target not in dict_search_args(firewall['ipv4'], 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, False) if 'ipv6' in firewall: - for name in ['ipv6_name','forward','input','output']: + for name in ['name','forward','input','output']: if name in firewall['ipv6']: for name_id, name_conf in firewall['ipv6'][name].items(): if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: raise ConfigError('default-action set to jump, but no default-jump-target specified') if 'default_jump_target' in name_conf: target = name_conf['default_jump_target'] if 'jump' not in name_conf['default_action']: raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') ## Now need to check that default-jump-target exists (other firewall chain/name) - if target not in dict_search_args(firewall['ipv6'], 'ipv6_name'): + if target not in dict_search_args(firewall['ipv6'], 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, True) return None def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True render(nftables_conf, 'firewall/nftables.j2', firewall) return None def apply_sysfs(firewall): for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) value = None if name in firewall['global_options']: conf_value = firewall['global_options'][name] if conf_value in conf: value = conf[conf_value] elif conf_value == 'enable': value = '1' elif conf_value == 'disable': value = '0' if value: for path in paths: with open(path, 'w') as f: f.write(value) def post_apply_trap(firewall): if 'first_install' in firewall: return None if 'config_trap' not in firewall['global_options'] or firewall['global_options']['config_trap'] != 'enable': return None if not process_named_running('snmpd'): return None trap_username = os.getlogin() for host, target_conf in firewall['trap_targets'].items(): community = target_conf['community'] if 'community' in target_conf else 'public' port = int(target_conf['port']) if 'port' in target_conf else 162 base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} ' for change_type, changes in firewall['trap_diff'].items(): for path_str, value in changes.items(): objects = [ f'mgmtEventUser s "{trap_username}"', f'mgmtEventSource i {snmp_event_source}', f'mgmtEventType i {snmp_change_type[change_type]}' ] if change_type == 'add': objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"') elif change_type == 'delete': objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"') elif change_type == 'change': objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"') objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"') cmd(base_cmd + ' '.join(objects)) def apply(firewall): install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: raise ConfigError(f'Failed to apply firewall: {output}') apply_sysfs(firewall) if firewall['group_resync']: call_dependents() # T970 Enable a resolver (systemd daemon) that checks # domain-group/fqdn addresses and update entries for domains by timeout # If router loaded without internet connection or for synchronization domain_action = 'stop' if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']: domain_action = 'restart' call(f'systemctl {domain_action} vyos-domain-resolver.service') if firewall['geoip_updated']: # Call helper script to Update set contents if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: print('Updating GeoIP. Please wait...') geoip_update(firewall) post_apply_trap(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/10-to-11 b/src/migration-scripts/firewall/10-to-11 index 8cd2a4df8..8afcb64fd 100755 --- a/src/migration-scripts/firewall/10-to-11 +++ b/src/migration-scripts/firewall/10-to-11 @@ -1,374 +1,374 @@ #!/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/>. # T5160: Firewall re-writing # cli changes from: # set firewall name <name> ... # set firewall ipv6-name <name> ... # To # set firewall ipv4 name <name> -# set firewall ipv6 ipv6-name <name> +# set firewall ipv6 name <name> ## Also from 'firewall interface' removed. ## in and out: # set firewall interface <iface> [in|out] [name | ipv6-name] <name> # To # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> [inbound-interface | outboubd-interface] interface-name <iface> # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> action jump # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> jump-target <name> ## local: # set firewall interface <iface> local [name | ipv6-name] <name> # To # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> inbound-interface interface-name <iface> # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> action jump # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> jump-target <name> import re from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.ifconfig import Section if (len(argv) < 1): print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) ### Migration of state policies if config.exists(base + ['state-policy']): for family in ['ipv4', 'ipv6']: for hook in ['forward', 'input', 'output']: for priority in ['filter']: # Add default-action== accept for compatibility reasons: config.set(base + [family, hook, priority, 'default-action'], value='accept') position = 1 for state in config.list_nodes(base + ['state-policy']): action = config.return_value(base + ['state-policy', state, 'action']) config.set(base + [family, hook, priority, 'rule']) config.set_tag(base + [family, hook, priority, 'rule']) config.set(base + [family, hook, priority, 'rule', position, 'state', state], value='enable') config.set(base + [family, hook, priority, 'rule', position, 'action'], value=action) position = position + 1 config.delete(base + ['state-policy']) ############ ## migration of global options: for option in ['all-ping', 'broadcast-ping', 'config-trap', 'ip-src-route', 'ipv6-receive-redirects', 'ipv6-src-route', 'log-martians', 'receive-redirects', 'resolver-cache', 'resolver-internal', 'send-redirects', 'source-validation', 'syn-cookies', 'twa-hazards-protection']: if config.exists(base + [option]): val = config.return_value(base + [option]) config.set(base + ['global-options', option], value=val) config.delete(base + [option]) ### Migration of firewall name and ipv6-name if config.exists(base + ['name']): config.set(['firewall', 'ipv4', 'name']) config.set_tag(['firewall', 'ipv4', 'name']) for ipv4name in config.list_nodes(base + ['name']): config.copy(base + ['name', ipv4name], base + ['ipv4', 'name', ipv4name]) config.delete(base + ['name']) if config.exists(base + ['ipv6-name']): - config.set(['firewall', 'ipv6', 'ipv6-name']) - config.set_tag(['firewall', 'ipv6', 'ipv6-name']) + config.set(['firewall', 'ipv6', 'name']) + config.set_tag(['firewall', 'ipv6', 'name']) for ipv6name in config.list_nodes(base + ['ipv6-name']): - config.copy(base + ['ipv6-name', ipv6name], base + ['ipv6', 'ipv6-name', ipv6name]) + config.copy(base + ['ipv6-name', ipv6name], base + ['ipv6', 'name', ipv6name]) config.delete(base + ['ipv6-name']) ### Migration of firewall interface if config.exists(base + ['interface']): fwd_ipv4_rule = 5 inp_ipv4_rule = 5 fwd_ipv6_rule = 5 inp_ipv6_rule = 5 for iface in config.list_nodes(base + ['interface']): for direction in ['in', 'out', 'local']: if config.exists(base + ['interface', iface, direction]): if config.exists(base + ['interface', iface, direction, 'name']): target = config.return_value(base + ['interface', iface, direction, 'name']) if direction == 'in': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target) fwd_ipv4_rule = fwd_ipv4_rule + 5 elif direction == 'out': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv4_rule, 'outbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target) fwd_ipv4_rule = fwd_ipv4_rule + 5 else: # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'input', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'input', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [inp_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [inp_ipv4_rule, 'action'], value='jump') config.set(new_base + [inp_ipv4_rule, 'jump-target'], value=target) inp_ipv4_rule = inp_ipv4_rule + 5 if config.exists(base + ['interface', iface, direction, 'ipv6-name']): target = config.return_value(base + ['interface', iface, direction, 'ipv6-name']) if direction == 'in': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv6', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target) fwd_ipv6_rule = fwd_ipv6_rule + 5 elif direction == 'out': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv6', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv6_rule, 'outbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target) fwd_ipv6_rule = fwd_ipv6_rule + 5 else: new_base = base + ['ipv6', 'input', 'filter', 'rule'] # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'input', 'filter', 'default-action'], value='accept') config.set(new_base) config.set_tag(new_base) config.set(new_base + [inp_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [inp_ipv6_rule, 'action'], value='jump') config.set(new_base + [inp_ipv6_rule, 'jump-target'], value=target) inp_ipv6_rule = inp_ipv6_rule + 5 config.delete(base + ['interface']) ### Migration of zones config v2: ### 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 v2 try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) \ No newline at end of file