From 450ca9a9b46d69036af432ddad316d4ddb126085 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:46:16 +0200 Subject: firewall: T2199: Refactor firewall + zone-policy, move interfaces under firewall node * Refactor firewall and zone-policy rule creation and cleanup * Migrate interface firewall values to `firewall interfaces name/ipv6-name ` * Remove `firewall-interface.py` conf script --- src/conf_mode/firewall-interface.py | 186 ---------------------------------- src/conf_mode/firewall.py | 193 +++++++----------------------------- src/conf_mode/zone_policy.py | 65 ++++-------- 3 files changed, 57 insertions(+), 387 deletions(-) delete mode 100755 src/conf_mode/firewall-interface.py (limited to 'src/conf_mode') diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py deleted file mode 100755 index ab1c69259..000000000 --- a/src/conf_mode/firewall-interface.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 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 . - -import os -import re - -from sys import argv -from sys import exit - -from vyos.config import Config -from vyos.configdict import leaf_node_changed -from vyos.ifconfig import Section -from vyos.template import render -from vyos.util import cmd -from vyos.util import dict_search_args -from vyos.util import run -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -NAME_PREFIX = 'NAME_' -NAME6_PREFIX = 'NAME6_' - -NFT_CHAINS = { - 'in': 'VYOS_FW_FORWARD', - 'out': 'VYOS_FW_FORWARD', - 'local': 'VYOS_FW_LOCAL' -} -NFT6_CHAINS = { - 'in': 'VYOS_FW6_FORWARD', - 'out': 'VYOS_FW6_FORWARD', - 'local': 'VYOS_FW6_LOCAL' -} - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - ifname = argv[1] - ifpath = Section.get_config_path(ifname) - if_firewall_path = f'interfaces {ifpath} firewall' - - if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if_firewall['ifname'] = ifname - if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - return if_firewall - -def verify_chain(table, chain): - # Verify firewall applied - code = run(f'nft list chain {table} {chain}') - return code == 0 - -def verify(if_firewall): - # bail out early - looks like removal from running config - if not if_firewall: - return None - - for direction in ['in', 'out', 'local']: - if direction in if_firewall: - if 'name' in if_firewall[direction]: - name = if_firewall[direction]['name'] - - if 'name' not in if_firewall['firewall']: - raise ConfigError('Firewall name not configured') - - if name not in if_firewall['firewall']['name']: - raise ConfigError(f'Invalid firewall name "{name}"') - - if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'): - raise ConfigError('Firewall did not apply') - - if 'ipv6_name' in if_firewall[direction]: - name = if_firewall[direction]['ipv6_name'] - - if 'ipv6_name' not in if_firewall['firewall']: - raise ConfigError('Firewall ipv6-name not configured') - - if name not in if_firewall['firewall']['ipv6_name']: - raise ConfigError(f'Invalid firewall ipv6-name "{name}"') - - if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'): - raise ConfigError('Firewall did not apply') - - return None - -def generate(if_firewall): - return None - -def cleanup_rule(table, chain, prefix, ifname, new_name=None): - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - retval = None - for line in results: - if f'{prefix}ifname "{ifname}"' in line: - if new_name and f'jump {new_name}' in line: - # new_name is used to clear rules for any previously referenced chains - # returns true when rule exists and doesn't need to be created - retval = True - continue - - handle_search = re.search('handle (\d+)', line) - if handle_search: - run(f'nft delete rule {table} {chain} handle {handle_search[1]}') - return retval - -def state_policy_handle(table, chain): - # Find any state-policy rule to ensure interface rules are only inserted afterwards - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - for line in results: - if 'jump VYOS_STATE_POLICY' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - return handle_search[1] - return None - -def apply(if_firewall): - ifname = if_firewall['ifname'] - - for direction in ['in', 'out', 'local']: - chain = NFT_CHAINS[direction] - ipv6_chain = NFT6_CHAINS[direction] - if_prefix = 'i' if direction in ['in', 'local'] else 'o' - - name = dict_search_args(if_firewall, direction, 'name') - if name: - rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}') - - if not rule_exists: - rule_action = 'insert' - rule_prefix = '' - - handle = state_policy_handle('ip filter', chain) - if handle: - rule_action = 'add' - rule_prefix = f'position {handle}' - - run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}') - else: - cleanup_rule('ip filter', chain, if_prefix, ifname) - - ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') - if ipv6_name: - rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}') - - if not rule_exists: - rule_action = 'insert' - rule_prefix = '' - - handle = state_policy_handle('ip6 filter', ipv6_chain) - if handle: - rule_action = 'add' - rule_prefix = f'position {handle}' - - run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}') - else: - cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname) - - 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/conf_mode/firewall.py b/src/conf_mode/firewall.py index f0ea1a1e5..86793ba86 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -26,6 +26,7 @@ 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.configverify import verify_interface_exists from vyos.firewall import geoip_update from vyos.firewall import get_ips_domains_dict from vyos.firewall import nft_add_set_elements @@ -38,7 +39,7 @@ from vyos.util import cmd from vyos.util import dict_search_args from vyos.util import dict_search_recursive from vyos.util import process_named_running -from vyos.util import run +from vyos.util import rc_cmd from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -47,7 +48,9 @@ airbag.enable() policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' nftables_conf = '/run/nftables.conf' -nftables_defines_conf = '/run/nftables_defines.conf' + +nftables_zone_conf = '/run/nftables_zone.conf' +nftables6_zone_conf = '/run/nftables_zone6.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, @@ -63,28 +66,6 @@ sysfs_config = { 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'} } -NAME_PREFIX = 'NAME_' -NAME6_PREFIX = 'NAME6_' - -preserve_chains = [ - 'INPUT', - 'FORWARD', - 'OUTPUT', - 'VYOS_FW_FORWARD', - 'VYOS_FW_LOCAL', - 'VYOS_FW_OUTPUT', - 'VYOS_POST_FW', - 'VYOS_FRAG_MARK', - 'VYOS_FW6_FORWARD', - 'VYOS_FW6_LOCAL', - 'VYOS_FW6_OUTPUT', - 'VYOS_POST_FW6', - 'VYOS_FRAG6_MARK' -] - -nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'] -nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] - valid_groups = [ 'address_group', 'domain_group', @@ -97,16 +78,6 @@ nested_group_types = [ 'port_group', 'ipv6_address_group', 'ipv6_network_group' ] -group_set_prefix = { - 'A_': 'address_group', - 'A6_': 'ipv6_address_group', - 'D_': 'domain_group', - 'M_': 'mac_group', - 'N_': 'network_group', - 'N6_': 'ipv6_network_group', - 'P_': 'port_group' -} - snmp_change_type = { 'unknown': 0, 'add': 1, @@ -117,22 +88,6 @@ snmp_event_source = 1 snmp_trap_mib = 'VYATTA-TRAP-MIB' snmp_trap_name = 'mgmtEventTrap' -def get_firewall_interfaces(conf): - out = {} - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - def find_interfaces(iftype_conf, output={}, prefix=''): - for ifname, if_conf in iftype_conf.items(): - if 'firewall' in if_conf: - output[prefix + ifname] = if_conf['firewall'] - for vif in ['vif', 'vif_s', 'vif_c']: - if vif in if_conf: - output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) - return output - for iftype, iftype_conf in interfaces.items(): - out.update(find_interfaces(iftype_conf)) - return out - def get_firewall_zones(conf): used_v4 = [] used_v6 = [] @@ -159,6 +114,8 @@ def get_firewall_zones(conf): ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') if ipv6_name: used_v6.append(ipv6_name) + else: + return None return {'name': used_v4, 'ipv6_name': used_v6} @@ -232,7 +189,6 @@ def get_config(config=None): firewall['ipv6_name'][ipv6_name]) firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) - firewall['interfaces'] = get_firewall_interfaces(conf) firewall['zone_policy'] = get_firewall_zones(conf) if 'config_trap' in firewall and firewall['config_trap'] == 'enable': @@ -358,109 +314,42 @@ def verify(firewall): for name in ['name', 'ipv6_name']: if name in firewall: for name_id, name_conf in firewall[name].items(): - if name_id in preserve_chains: - raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS') - - if name_id.startswith("VZONE"): - raise ConfigError(f'Firewall name "{name_id}" uses reserved prefix') - if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, name == 'ipv6_name') - for ifname, if_firewall in firewall['interfaces'].items(): - for direction in ['in', 'out', 'local']: - name = dict_search_args(if_firewall, direction, 'name') - ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') + if 'interface' in firewall: + for ifname, if_firewall in firewall['interface'].items(): + # verify ifname needs to be disabled, dynamic devices come up later + # verify_interface_exists(ifname) - if name and dict_search_args(firewall, 'name', name) == None: - raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}') + for direction in ['in', 'out', 'local']: + name = dict_search_args(if_firewall, direction, 'name') + ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') - if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: - raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}') + if name and dict_search_args(firewall, 'name', name) == None: + raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}') - for fw_name, used_names in firewall['zone_policy'].items(): - for name in used_names: - if dict_search_args(firewall, fw_name, name) == None: - raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') + if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: + raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}') - return None + if firewall['zone_policy']: + for fw_name, used_names in firewall['zone_policy'].items(): + for name in used_names: + if dict_search_args(firewall, fw_name, name) == None: + raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') -def cleanup_commands(firewall): - commands = [] - commands_chains = [] - commands_sets = [] - for table in ['ip filter', 'ip6 filter']: - name_node = 'name' if table == 'ip filter' else 'ipv6_name' - chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX - state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6' - iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains - - geoip_list = [] - if firewall['geoip_updated']: - geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' - geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] - - json_str = cmd(f'nft -t -j list table {table}') - obj = loads(json_str) - - if 'nftables' not in obj: - continue - - for item in obj['nftables']: - if 'chain' in item: - chain = item['chain']['name'] - if chain in preserve_chains or chain.startswith("VZONE"): - continue - - if chain == state_chain: - command = 'delete' if 'state_policy' not in firewall else 'flush' - commands_chains.append(f'{command} chain {table} {chain}') - elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - else: - commands_chains.append(f'delete chain {table} {chain}') - - if 'rule' in item: - rule = item['rule'] - chain = rule['chain'] - handle = rule['handle'] - - if chain in iface_chains: - target, _ = next(dict_search_recursive(rule['expr'], 'target')) - - if target == state_chain and 'state_policy' not in firewall: - commands.append(f'delete rule {table} {chain} handle {handle}') - - if target.startswith(chain_prefix): - if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None: - commands.append(f'delete rule {table} {chain} handle {handle}') - - if 'set' in item: - set_name = item['set']['name'] - - if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: - commands_sets.append(f'delete set {table} {set_name}') - continue - - if set_name.startswith("RECENT_"): - commands_sets.append(f'delete set {table} {set_name}') - continue - - for prefix, group_type in group_set_prefix.items(): - if set_name.startswith(prefix): - group_name = set_name.replace(prefix, "", 1) - if dict_search_args(firewall, 'group', group_type, group_name) != None: - commands_sets.append(f'flush set {table} {set_name}') - else: - commands_sets.append(f'delete set {table} {set_name}') - return commands + commands_chains + commands_sets + return None def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True - else: - firewall['cleanup_commands'] = cleanup_commands(firewall) + + if os.path.exists(nftables_zone_conf): + firewall['zone_conf'] = nftables_zone_conf + + if os.path.exists(nftables6_zone_conf): + firewall['zone6_conf'] = nftables6_zone_conf render(nftables_conf, 'firewall/nftables.j2', firewall) return None @@ -521,26 +410,21 @@ def post_apply_trap(firewall): cmd(base_cmd + ' '.join(objects)) -def state_policy_rule_exists(): - # Determine if state policy rules already exist in nft - search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD') - return 'VYOS_STATE_POLICY' in search_str - def resync_policy_route(): # Update policy route as firewall groups were updated - tmp = run(policy_route_conf_script) + tmp, out = rc_cmd(policy_route_conf_script) if tmp > 0: - Warning('Failed to re-apply policy route configuration!') + Warning(f'Failed to re-apply policy route configuration! {out}') def apply(firewall): if 'first_install' in firewall: run('nfct helper add rpc inet tcp') run('nfct helper add rpc inet udp') run('nfct helper add tns inet tcp') - - install_result = run(f'nft -f {nftables_conf}') + + install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: - raise ConfigError('Failed to apply firewall') + raise ConfigError(f'Failed to apply firewall: {output}') # set firewall group domain-group xxx if 'group' in firewall: @@ -563,13 +447,6 @@ def apply(firewall): else: call('systemctl stop vyos-domain-group-resolve.service') - if 'state_policy' in firewall and not state_policy_rule_exists(): - for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']: - cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') - - for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: - cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6') - apply_sysfs(firewall) if firewall['policy_resync']: diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index a52c52706..c6ab4e304 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -21,6 +21,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdiff import get_config_diff from vyos.template import render from vyos.util import cmd from vyos.util import dict_search_args @@ -30,7 +31,9 @@ from vyos import ConfigError from vyos import airbag airbag.enable() +firewall_conf_script = '/usr/libexec/vyos/conf_mode/firewall.py' nftables_conf = '/run/nftables_zone.conf' +nftables6_conf = '/run/nftables_zone6.conf' def get_config(config=None): if config: @@ -47,6 +50,9 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) + diff = get_config_diff(conf) + zone_policy['firewall_changed'] = diff.is_node_changed(['firewall']) + if 'zone' in zone_policy: # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. @@ -111,20 +117,12 @@ def verify(zone_policy): raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') v4_name = dict_search_args(from_conf, 'firewall', 'name') - if v4_name: - if 'name' not in zone_policy['firewall']: - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - if not dict_search_args(zone_policy, 'firewall', 'name', v4_name): - raise ConfigError(f'Firewall name "{v4_name}" does not exist') + if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') v6_name = dict_search_args(from_conf, 'firewall', 'v6_name') - if v6_name: - if 'ipv6_name' not in zone_policy['firewall']: - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - - if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') return None @@ -152,37 +150,11 @@ def get_local_from(zone_policy, local_zone_name): out[zone] = zone_conf['from'][local_zone_name] return out -def cleanup_commands(): - commands = [] - for table in ['ip filter', 'ip6 filter']: - json_str = cmd(f'nft -t -j list table {table}') - obj = loads(json_str) - if 'nftables' not in obj: - continue - for item in obj['nftables']: - if 'rule' in item: - chain = item['rule']['chain'] - handle = item['rule']['handle'] - if 'expr' not in item['rule']: - continue - for expr in item['rule']['expr']: - target = dict_search_args(expr, 'jump', 'target') - if not target: - continue - if target.startswith("VZONE") or target.startswith("VYOS_STATE_POLICY"): - commands.append(f'delete rule {table} {chain} handle {handle}') - for item in obj['nftables']: - if 'chain' in item: - if item['chain']['name'].startswith("VZONE"): - chain = item['chain']['name'] - commands.append(f'delete chain {table} {chain}') - return commands - def generate(zone_policy): data = zone_policy or {} - if os.path.exists(nftables_conf): # Check to see if we've run before - data['cleanup_commands'] = cleanup_commands() + if not os.path.exists(nftables_conf): + data['first_install'] = True if 'zone' in data: for zone, zone_conf in data['zone'].items(): @@ -193,12 +165,19 @@ def generate(zone_policy): zone_conf['from_local'] = get_local_from(data, zone) render(nftables_conf, 'zone_policy/nftables.j2', data) + render(nftables6_conf, 'zone_policy/nftables6.j2', data) return None +def update_firewall(): + # Update firewall to refresh nftables + tmp = run(firewall_conf_script) + if tmp > 0: + Warning('Failed to update firewall configuration!') + def apply(zone_policy): - install_result = run(f'nft -f {nftables_conf}') - if install_result != 0: - raise ConfigError('Failed to apply zone-policy') + # If firewall will not update in this commit, we need to call the conf script + if not zone_policy['firewall_changed']: + update_firewall() return None -- cgit v1.2.3