diff options
Diffstat (limited to 'src/conf_mode/firewall.py')
-rwxr-xr-x | src/conf_mode/firewall.py | 292 |
1 files changed, 110 insertions, 182 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f0ea1a1e5..cbd9cbe90 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,6 @@ 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' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, @@ -63,28 +63,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 +75,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,51 +85,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 = [] - zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if 'zone' in zone_policy: - for zone, zone_conf in zone_policy['zone'].items(): - if 'from' in zone_conf: - for from_zone, from_conf in zone_conf['from'].items(): - name = dict_search_args(from_conf, 'firewall', 'name') - if name: - used_v4.append(name) - - ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') - if ipv6_name: - used_v6.append(ipv6_name) - - if 'intra_zone_filtering' in zone_conf: - name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name') - if name: - used_v4.append(name) - - ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') - if ipv6_name: - used_v6.append(ipv6_name) - - return {'name': used_v4, 'ipv6_name': used_v6} - def geoip_updated(conf, firewall): diff = get_config_diff(conf) node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) @@ -215,6 +138,9 @@ def get_config(config=None): if tmp in default_values: del default_values[tmp] + if 'zone' in default_values: + del default_values['zone'] + firewall = dict_merge(default_values, firewall) # Merge in defaults for IPv4 ruleset @@ -231,9 +157,12 @@ def get_config(config=None): firewall['ipv6_name'][ipv6_name] = dict_merge(default_values, firewall['ipv6_name'][ipv6_name]) + if 'zone' in firewall: + default_values = defaults(base + ['zone']) + for zone in firewall['zone']: + firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) + firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) - firewall['interfaces'] = get_firewall_interfaces(conf) - firewall['zone_policy'] = get_firewall_zones(conf) if 'config_trap' in firewall and firewall['config_trap'] == 'enable': diff = get_config_diff(conf) @@ -250,6 +179,20 @@ 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, '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_name'): + raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system') + 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"') @@ -358,109 +301,111 @@ 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 '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, 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, 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 + local_zone = False + zone_interfaces = [] -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 'zone' in firewall: + for zone, zone_conf in firewall['zone'].items(): + if 'local_zone' not in zone_conf and 'interface' not in zone_conf: + raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') + + if 'local_zone' in zone_conf: + if local_zone: + raise ConfigError('There cannot be multiple local zones') + if 'interface' in zone_conf: + raise ConfigError('Local zone cannot have interfaces assigned') + if 'intra_zone_filtering' in zone_conf: + raise ConfigError('Local zone cannot use intra-zone-filtering') + local_zone = True - if 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 'interface' in zone_conf: + found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] - if 'rule' in item: - rule = item['rule'] - chain = rule['chain'] - handle = rule['handle'] + if found_duplicates: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - if chain in iface_chains: - target, _ = next(dict_search_recursive(rule['expr'], 'target')) + zone_interfaces += zone_conf['interface'] - if target == state_chain and 'state_policy' not in firewall: - commands.append(f'delete rule {table} {chain} handle {handle}') + if 'intra_zone_filtering' in zone_conf: + intra_zone = zone_conf['intra_zone_filtering'] - 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 len(intra_zone) > 1: + raise ConfigError('Only one intra-zone-filtering action must be specified') - if 'set' in item: - set_name = item['set']['name'] + if 'firewall' in intra_zone: + v4_name = dict_search_args(intra_zone, 'firewall', 'name') + if v4_name and not dict_search_args(firewall, 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') - if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: - commands_sets.append(f'delete set {table} {set_name}') - continue + v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name') + if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - if set_name.startswith("RECENT_"): - commands_sets.append(f'delete set {table} {set_name}') - continue + if not v4_name and not v6_name: + raise ConfigError('No firewall names specified for intra-zone-filtering') + + if 'from' in zone_conf: + for from_zone, from_conf in zone_conf['from'].items(): + if from_zone not in firewall['zone']: + raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') + + v4_name = dict_search_args(from_conf, 'firewall', 'name') + if v4_name and not dict_search_args(firewall, 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') + if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - 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 'zone' in firewall: + for local_zone, local_zone_conf in firewall['zone'].items(): + if 'local_zone' not in local_zone_conf: + continue + + local_zone_conf['from_local'] = {} + + for zone, zone_conf in firewall['zone'].items(): + if zone == local_zone or 'from' not in zone_conf: + continue + if local_zone in zone_conf['from']: + local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] render(nftables_conf, 'firewall/nftables.j2', firewall) return None @@ -521,26 +466,16 @@ 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 +498,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']: |