diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/configdict.py | 49 | ||||
-rw-r--r-- | python/vyos/configverify.py | 12 | ||||
-rw-r--r-- | python/vyos/firewall.py | 225 | ||||
-rw-r--r-- | python/vyos/ifconfig/bond.py | 75 | ||||
-rw-r--r-- | python/vyos/ifconfig/bridge.py | 21 | ||||
-rw-r--r--[-rwxr-xr-x] | python/vyos/ifconfig/interface.py | 52 | ||||
-rw-r--r-- | python/vyos/ifconfig/vti.py | 6 | ||||
-rw-r--r-- | python/vyos/pki.py | 61 | ||||
-rw-r--r-- | python/vyos/template.py | 27 | ||||
-rw-r--r-- | python/vyos/util.py | 4 | ||||
-rw-r--r-- | python/vyos/validate.py | 19 |
11 files changed, 481 insertions, 70 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 04ddc10e9..a61666afc 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -201,11 +201,12 @@ def is_member(conf, interface, intftype=None): intftype is optional, if not passed it will search all known types (currently bridge and bonding) - Returns: - None -> Interface is not a member - interface name -> Interface is a member of this interface - False -> interface type cannot have members + Returns: dict + empty -> Interface is not a member + key -> Interface is a member of this interface """ + from vyos.ifconfig import Section + ret_val = {} intftypes = ['bonding', 'bridge'] @@ -221,9 +222,24 @@ def is_member(conf, interface, intftype=None): for intf in conf.list_nodes(base): member = base + [intf, 'member', 'interface', interface] if conf.exists(member): - tmp = conf.get_config_dict(member, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - ret_val.update({intf : tmp}) + member_type = Section.section(interface) + # Check if it's a VLAN (QinQ) interface + interface = interface.split('.') + if len(interface) == 3: + if conf.exists(['interfaces', member_type, interface[0], 'vif-s', interface[1], 'vif-c', interface[2]]): + tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], + key_mangling=('-', '_'), get_first_key=True) + ret_val.update({intf : tmp}) + elif len(interface) == 2: + if conf.exists(['interfaces', member_type, interface[0], 'vif', interface[1]]): + tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], + key_mangling=('-', '_'), get_first_key=True) + ret_val.update({intf : tmp}) + else: + if conf.exists(['interfaces', member_type, interface[0]]): + tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], + key_mangling=('-', '_'), get_first_key=True) + ret_val.update({intf : tmp}) return ret_val @@ -358,13 +374,14 @@ def get_pppoe_interfaces(conf, vrf=None): """ Common helper functions to retrieve all interfaces from current CLI sessions that have DHCP configured. """ pppoe_interfaces = {} + conf.set_level([]) for ifname in conf.list_nodes(['interfaces', 'pppoe']): # always reset config level, as get_interface_dict() will alter it conf.set_level([]) # we already have a dict representation of the config from get_config_dict(), # but with the extended information from get_interface_dict() we also # get the DHCP client default-route-distance default option if not specified. - ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname) + _, ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname) options = {} if 'default_route_distance' in ifconfig: @@ -455,8 +472,8 @@ def get_interface_dict(config, base, ifname=''): if bond: dict.update({'is_bond_member' : bond}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['dhcp-options'], recursive=True) - if dhcp: dict.update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'dhcp-options']) + if dhcp: dict.update({'dhcp_options_changed' : {}}) # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the @@ -515,8 +532,8 @@ def get_interface_dict(config, base, ifname=''): if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif', vif, 'dhcp-options'], recursive=True) - if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options']) + if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}}) for vif_s, vif_s_config in dict.get('vif_s', {}).items(): # Add subinterface name to dictionary @@ -554,8 +571,8 @@ def get_interface_dict(config, base, ifname=''): if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif-s', vif_s, 'dhcp-options'], recursive=True) - if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options']) + if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}}) for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): # Add subinterface name to dictionary @@ -594,8 +611,8 @@ def get_interface_dict(config, base, ifname=''): {'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'], recursive=True) - if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options']) + if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}}) # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, base + [ifname], dict) diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 438485d98..137eb9f79 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -99,6 +99,18 @@ def verify_vrf(config): 'Interface "{ifname}" cannot be both a member of VRF "{vrf}" ' 'and bridge "{is_bridge_member}"!'.format(**config)) +def verify_bond_bridge_member(config): + """ + Checks if interface has a VRF configured and is also part of a bond or + bridge, which is not allowed! + """ + if 'vrf' in config: + ifname = config['ifname'] + if 'is_bond_member' in config: + raise ConfigError(f'Can not add interface "{ifname}" to bond, it has a VRF assigned!') + if 'is_bridge_member' in config: + raise ConfigError(f'Can not add interface "{ifname}" to bridge, it has a VRF assigned!') + def verify_tunnel(config): """ This helper is used to verify the common part of the tunnel diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 04fd44173..3e2de4c3f 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# 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 @@ -14,10 +14,82 @@ # 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 time import strftime + +from vyos.remote import download +from vyos.template import is_ipv4 +from vyos.template import render +from vyos.util import call from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive +from vyos.util import run + + +# Functions for firewall group domain-groups +def get_ips_domains_dict(list_domains): + """ + Get list of IPv4 addresses by list of domains + Ex: get_ips_domains_dict(['ex1.com', 'ex2.com']) + {'ex1.com': ['192.0.2.1'], 'ex2.com': ['192.0.2.2', '192.0.2.3']} + """ + from socket import gethostbyname_ex + from socket import gaierror + + ip_dict = {} + for domain in list_domains: + try: + _, _, ips = gethostbyname_ex(domain) + ip_dict[domain] = ips + except gaierror: + pass + + return ip_dict + +def nft_init_set(group_name, table="filter", family="ip"): + """ + table ip filter { + set GROUP_NAME + type ipv4_addr + flags interval + } + """ + return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}') + + +def nft_add_set_elements(group_name, elements, table="filter", family="ip"): + """ + table ip filter { + set GROUP_NAME { + type ipv4_addr + flags interval + elements = { 192.0.2.1, 192.0.2.2 } + } + """ + elements = ", ".join(elements) + return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ') + +def nft_flush_set(group_name, table="filter", family="ip"): + """ + Flush elements of nft set + """ + return call(f'nft flush set {family} {table} {group_name}') + +def nft_update_set_elements(group_name, elements, table="filter", family="ip"): + """ + Update elements of nft set + """ + flush_set = nft_flush_set(group_name, table="filter", family="ip") + nft_add_set = nft_add_set_elements(group_name, elements, table="filter", family="ip") + return flush_set, nft_add_set + +# END firewall group domain-group (sets) def find_nftables_rule(table, chain, rule_matches=[]): # Find rule in table/chain that matches all criteria and return the handle @@ -78,6 +150,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if suffix[0] == '!': suffix = f'!= {suffix[1:]}' output.append(f'{ip_name} {prefix}addr {suffix}') + + if dict_search_args(side_conf, 'geoip', 'country_code'): + operator = '' + if dict_search_args(side_conf, 'geoip', 'inverse_match') != None: + operator = '!=' + output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC_{fw_name}_{rule_id}') if 'mac_address' in side_conf: suffix = side_conf["mac_address"] @@ -117,21 +195,29 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_name} {prefix}addr {operator} $A{def_suffix}_{group_name}') + 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}') + 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}') + output.append(f'ether {prefix}addr {operator} @M_{group_name}') if 'port_group' in group: proto = rule_conf['protocol'] group_name = group['port_group'] @@ -144,11 +230,16 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): operator = '!=' group_name = group_name[1:] - output.append(f'{proto} {prefix}port {operator} $P_{group_name}') + 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()}] "') + output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') + + if 'log_level' in rule_conf: + log_level = rule_conf['log_level'] + output.append(f'level {log_level}') + if 'hop_limit' in rule_conf: operators = {'eq': '==', 'gt': '>', 'lt': '<'} @@ -157,6 +248,13 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): value = rule_conf['hop_limit'][op] output.append(f'ip6 hoplimit {operator} {value}') + 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]: @@ -257,3 +355,118 @@ def parse_policy_set(set_conf, def_suffix): 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[3]}' + if path[0] == 'name': + for code in codes: + ipv4_codes.setdefault(code, []).append(set_name) + elif path[0] == 'ipv6_name': + 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/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 2b9afe109..98bf6162b 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -179,6 +179,21 @@ class BondIf(Interface): """ self.set_interface('bond_lacp_rate', slow_fast) + def set_miimon_interval(self, interval): + """ + Specifies the MII link monitoring frequency in milliseconds. This + determines how often the link state of each slave is inspected for link + failures. A value of zero disables MII link monitoring. A value of 100 + is a good starting point. + + The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_miimon_interval('100') + """ + return self.set_interface('bond_miimon', interval) + def set_arp_interval(self, interval): """ Specifies the ARP link monitoring frequency in milliseconds. @@ -202,16 +217,7 @@ class BondIf(Interface): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').set_arp_interval('100') """ - if int(interval) == 0: - """ - Specifies the MII link monitoring frequency in milliseconds. - This determines how often the link state of each slave is - inspected for link failures. A value of zero disables MII - link monitoring. A value of 100 is a good starting point. - """ - return self.set_interface('bond_miimon', interval) - else: - return self.set_interface('bond_arp_interval', interval) + return self.set_interface('bond_arp_interval', interval) def get_arp_ip_target(self): """ @@ -381,26 +387,9 @@ class BondIf(Interface): if 'shutdown_required' in config: self.set_admin_state('down') - # ARP monitor targets need to be synchronized between sysfs and CLI. - # Unfortunately an address can't be send twice to sysfs as this will - # result in the following exception: OSError: [Errno 22] Invalid argument. - # - # We remove ALL addresses prior to adding new ones, this will remove - # addresses manually added by the user too - but as we are limited to 16 adresses - # from the kernel side this looks valid to me. We won't run into an error - # when a user added manual adresses which would result in having more - # then 16 adresses in total. - arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) - for addr in arp_tgt_addr: - self.set_arp_ip_target('-' + addr) - - # Add configured ARP target addresses - value = dict_search('arp_monitor.target', config) - if isinstance(value, str): - value = [value] - if value: - for addr in value: - self.set_arp_ip_target('+' + addr) + # Specifies the MII link monitoring frequency in milliseconds + value = config.get('mii_mon_interval') + self.set_miimon_interval(value) # Bonding transmit hash policy value = config.get('hash_policy') @@ -430,6 +419,32 @@ class BondIf(Interface): if mode == '802.3ad': self.set_lacp_rate(config.get('lacp_rate')) + if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']: + tmp = dict_search('arp_monitor.interval', config) + value = tmp if (tmp != None) else '0' + self.set_arp_interval(value) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = dict_search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + # Add (enslave) interfaces to bond value = dict_search('member.interface', config) for interface in (value or []): diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index ffd9c590f..e4db69c1f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -90,6 +90,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', }, + 'multicast_snooping': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_snooping', + }, }} _command_set = {**Interface._command_set, **{ @@ -198,6 +202,18 @@ class BridgeIf(Interface): """ self.set_interface('multicast_querier', enable) + def set_multicast_snooping(self, enable): + """ + Enable or disable multicast snooping on the bridge. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_snooping(1) + """ + self.set_interface('multicast_snooping', enable) + def add_port(self, interface): """ Add physical interface to bridge (member port) @@ -257,6 +273,11 @@ class BridgeIf(Interface): value = '1' if 'stp' in config else '0' self.set_stp(value) + # enable or disable multicast snooping + tmp = dict_search('igmp.snooping', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_snooping(value) + # enable or disable IGMP querier tmp = dict_search('igmp.querier', config) value = '1' if (tmp != None) else '0' diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 22441d1d2..555494f80 100755..100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -168,6 +168,10 @@ class Interface(Control): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding', }, + 'ipv4_directed_broadcast': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', + }, 'rp_filter': { 'validate': lambda flt: assert_range(flt,0,3), 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', @@ -234,6 +238,9 @@ class Interface(Control): 'ipv4_forwarding': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding', }, + 'ipv4_directed_broadcast': { + 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', + }, 'rp_filter': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', }, @@ -713,6 +720,13 @@ class Interface(Control): return None return self.set_interface('ipv4_forwarding', forwarding) + def set_ipv4_directed_broadcast(self, forwarding): + """ Configure IPv4 directed broadcast forwarding. """ + tmp = self.get_interface('ipv4_directed_broadcast') + if tmp == forwarding: + return None + return self.set_interface('ipv4_directed_broadcast', forwarding) + def set_ipv4_source_validation(self, value): """ Help prevent attacks used by Spoofing IP Addresses. Reverse path @@ -1305,8 +1319,9 @@ class Interface(Control): # clear existing ingess - ignore errors (e.g. "Error: Cannot find specified # qdisc on specified device") - we simply cleanup all stuff here - self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null'); - self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null'); + if not 'traffic_policy' in self._config: + self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null'); + self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null'); # Apply interface mirror policy if mirror_config: @@ -1439,14 +1454,22 @@ class Interface(Control): if dhcpv6pd: self.set_dhcpv6(True) - # There are some items in the configuration which can only be applied - # if this instance is not bound to a bridge. This should be checked - # by the caller but better save then sorry! - if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): - # Bind interface to given VRF or unbind it if vrf node is not set. - # unbinding will call 'ip link set dev eth0 nomaster' which will - # also drop the interface out of a bridge or bond - thus this is - # checked before + # XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding + # will call 'ip link set dev eth0 nomaster' which will also drop the + # interface out of any bridge or bond - thus this is checked before. + if 'is_bond_member' in config: + bond_if = next(iter(config['is_bond_member'])) + tmp = get_interface_config(config['ifname']) + if 'master' in tmp and tmp['master'] != bond_if: + self.set_vrf('') + + elif 'is_bridge_member' in config: + bridge_if = next(iter(config['is_bridge_member'])) + tmp = get_interface_config(config['ifname']) + if 'master' in tmp and tmp['master'] != bridge_if: + self.set_vrf('') + + else: self.set_vrf(config.get('vrf', '')) # Add this section after vrf T4331 @@ -1498,6 +1521,11 @@ class Interface(Control): value = '0' if (tmp != None) else '1' self.set_ipv4_forwarding(value) + # IPv4 directed broadcast forwarding + tmp = dict_search('ip.enable_directed_broadcast', config) + value = '1' if (tmp != None) else '0' + self.set_ipv4_directed_broadcast(value) + # IPv4 source-validation tmp = dict_search('ip.source_validation', config) value = tmp if (tmp != None) else '0' @@ -1555,8 +1583,8 @@ class Interface(Control): # re-add ourselves to any bridge we might have fallen out of if 'is_bridge_member' in config: - bridge_dict = config.get('is_bridge_member') - self.add_to_bridge(bridge_dict) + tmp = config.get('is_bridge_member') + self.add_to_bridge(tmp) # eXpress Data Path - highly experimental self.set_xdp('xdp' in config) diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index c50cd5ce9..dc99d365a 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -53,3 +53,7 @@ class VTIIf(Interface): self._cmd(cmd.format(**self.config)) self.set_interface('admin_state', 'down') + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() diff --git a/python/vyos/pki.py b/python/vyos/pki.py index fd91fc9bf..cd15e3878 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -332,6 +332,54 @@ def verify_certificate(cert, ca_cert): except InvalidSignature: return False +def verify_crl(crl, ca_cert): + # Verify CRL was signed by specified CA + if ca_cert.subject != crl.issuer: + return False + + ca_public_key = ca_cert.public_key() + try: + if isinstance(ca_public_key, rsa.RSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + padding=padding.PKCS1v15(), + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, dsa.DSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, ec.EllipticCurvePublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + signature_algorithm=ec.ECDSA(crl.signature_hash_algorithm)) + else: + return False # We cannot verify it + return True + except InvalidSignature: + return False + +def verify_ca_chain(sorted_names, pki_node): + if len(sorted_names) == 1: # Single cert, no chain + return True + + for name in sorted_names: + cert = load_certificate(pki_node[name]['certificate']) + verified = False + for ca_name in sorted_names: + if name == ca_name: + continue + ca_cert = load_certificate(pki_node[ca_name]['certificate']) + if verify_certificate(cert, ca_cert): + verified = True + break + if not verified and name != sorted_names[-1]: + # Only permit top-most certificate to fail verify (e.g. signed by public CA not explicitly in chain) + return False + return True + # Certificate chain def find_parent(cert, ca_certs): @@ -357,3 +405,16 @@ def find_chain(cert, ca_certs): chain.append(parent) return chain + +def sort_ca_chain(ca_names, pki_node): + def ca_cmp(ca_name1, ca_name2, pki_node): + cert1 = load_certificate(pki_node[ca_name1]['certificate']) + cert2 = load_certificate(pki_node[ca_name2]['certificate']) + + if verify_certificate(cert1, cert2): # cert1 is child of cert2 + return -1 + return 1 + + from functools import cmp_to_key + return sorted(ca_names, key=cmp_to_key(lambda cert1, cert2: ca_cmp(cert1, cert2, pki_node))) + diff --git a/python/vyos/template.py b/python/vyos/template.py index 132f5ddde..eb7f06480 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -554,7 +554,7 @@ def nft_default_rule(fw_conf, fw_name): if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() - output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}] "') + output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}]"') output.append(nft_action(default_action)) output.append(f'comment "{fw_name} default-action {default_action}"') @@ -564,8 +564,9 @@ def nft_default_rule(fw_conf, fw_name): def nft_state_policy(conf, state, ipv6=False): out = [f'ct state {state}'] - if 'log' in conf and 'enable' in conf['log']: - out.append('log') + if 'log' in conf: + log_level = conf['log'] + out.append(f'log level {log_level}') out.append('counter') @@ -590,6 +591,26 @@ def nft_intra_zone_action(zone_conf, ipv6=False): return f'jump {name_prefix}{name}' return 'return' +@register_filter('nft_nested_group') +def nft_nested_group(out_list, includes, groups, key): + if not vyos_defined(out_list): + out_list = [] + + def add_includes(name): + if key in groups[name]: + for item in groups[name][key]: + if item in out_list: + continue + out_list.append(item) + + if 'include' in groups[name]: + for name_inc in groups[name]['include']: + add_includes(name_inc) + + for name in includes: + add_includes(name) + return out_list + @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ diff --git a/python/vyos/util.py b/python/vyos/util.py index 0d62fbfe9..bee5d7aec 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -197,7 +197,7 @@ def read_file(fname, defaultonfailure=None): return defaultonfailure raise e -def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None): +def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False): """ Write content of data to given fname, should defaultonfailure be not None, it is returned on failure to read. @@ -212,7 +212,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N try: """ Write a file to string """ bytes = 0 - with open(fname, 'w') as f: + with open(fname, 'w' if not append else 'a') as f: bytes = f.write(data) chown(fname, user, group) chmod(fname, mode) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index e005da0e4..a83193363 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -264,3 +264,22 @@ def has_address_configured(conf, intf): conf.set_level(old_level) return ret + +def has_vrf_configured(conf, intf): + """ + Checks if interface has a VRF configured. + + Returns True if interface has VRF configured, False if it doesn't. + """ + from vyos.ifconfig import Section + ret = False + + old_level = conf.get_level() + conf.set_level([]) + + tmp = ['interfaces', Section.get_config_path(intf), 'vrf'] + if conf.exists(tmp): + ret = True + + conf.set_level(old_level) + return ret |