summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configdict.py43
-rw-r--r--python/vyos/configsession.py4
-rw-r--r--python/vyos/cpu.py102
-rw-r--r--python/vyos/firewall.py234
-rw-r--r--python/vyos/frr.py2
-rw-r--r--python/vyos/ifconfig/bond.py75
-rw-r--r--python/vyos/ifconfig/bridge.py21
-rw-r--r--[-rwxr-xr-x]python/vyos/ifconfig/interface.py19
-rw-r--r--python/vyos/ifconfig/vti.py6
-rw-r--r--python/vyos/migrator.py5
-rw-r--r--python/vyos/pki.py63
-rw-r--r--python/vyos/template.py27
-rw-r--r--python/vyos/util.py21
13 files changed, 556 insertions, 66 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 04ddc10e9..29d89520c 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,18 @@ 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]]):
+ ret_val.update({intf : {}})
+ elif len(interface) == 2:
+ if conf.exists(['interfaces', member_type, interface[0], 'vif', interface[1]]):
+ ret_val.update({intf : {}})
+ else:
+ if conf.exists(['interfaces', member_type, interface[0]]):
+ ret_val.update({intf : {}})
return ret_val
@@ -358,13 +368,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 +466,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 +526,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 +565,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 +605,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/configsession.py b/python/vyos/configsession.py
index f28ad09c5..3a60f6d92 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -33,6 +33,7 @@ INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
+RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset']
# Default "commit via" string
APP = "vyos-http-api"
@@ -200,3 +201,6 @@ class ConfigSession(object):
out = self.__run_command(SHOW + path)
return out
+ def reset(self, path):
+ out = self.__run_command(RESET + path)
+ return out
diff --git a/python/vyos/cpu.py b/python/vyos/cpu.py
new file mode 100644
index 000000000..a0ef864be
--- /dev/null
+++ b/python/vyos/cpu.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# Copyright 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Retrieves (or at least attempts to retrieve) the total number of real CPU cores
+installed in a Linux system.
+
+The issue of core count is complicated by existence of SMT, e.g. Intel's Hyper Threading.
+GNU nproc returns the number of LOGICAL cores,
+which is 2x of the real cores if SMT is enabled.
+
+The idea is to find all physical CPUs and add up their core counts.
+It has special cases for x86_64 and MAY work correctly on other architectures,
+but nothing is certain.
+"""
+
+import re
+
+
+def _read_cpuinfo():
+ with open('/proc/cpuinfo', 'r') as f:
+ return f.readlines()
+
+def _split_line(l):
+ l = l.strip()
+ parts = re.split(r'\s*:\s*', l)
+ return (parts[0], ":".join(parts[1:]))
+
+def _find_cpus(cpuinfo_lines):
+ # Make a dict because it's more convenient to work with later,
+ # when we need to find physicall distinct CPUs there.
+ cpus = {}
+
+ cpu_number = 0
+
+ for l in cpuinfo_lines:
+ key, value = _split_line(l)
+ if key == 'processor':
+ cpu_number = value
+ cpus[cpu_number] = {}
+ else:
+ cpus[cpu_number][key] = value
+
+ return cpus
+
+def _find_physical_cpus():
+ cpus = _find_cpus(_read_cpuinfo())
+
+ phys_cpus = {}
+
+ for num in cpus:
+ if 'physical id' in cpus[num]:
+ # On at least some architectures, CPUs in different sockets
+ # have different 'physical id' field, e.g. on x86_64.
+ phys_id = cpus[num]['physical id']
+ if phys_id not in phys_cpus:
+ phys_cpus[phys_id] = cpus[num]
+ else:
+ # On other architectures, e.g. on ARM, there's no such field.
+ # We just assume they are different CPUs,
+ # whether single core ones or cores of physical CPUs.
+ phys_cpus[num] = cpu[num]
+
+ return phys_cpus
+
+def get_cpus():
+ """ Returns a list of /proc/cpuinfo entries that belong to different CPUs.
+ """
+ cpus_dict = _find_physical_cpus()
+ return list(cpus_dict.values())
+
+def get_core_count():
+ """ Returns the total number of physical CPU cores
+ (even if Hyper-Threading or another SMT is enabled and has inflated
+ the number of cores in /proc/cpuinfo)
+ """
+ physical_cpus = _find_physical_cpus()
+
+ core_count = 0
+
+ for num in physical_cpus:
+ # Some architectures, e.g. x86_64, include a field for core count.
+ # Since we found unique physical CPU entries, we can sum their core counts.
+ if 'cpu cores' in physical_cpus[num]:
+ core_count += int(physical_cpus[num]['cpu cores'])
+ else:
+ core_count += 1
+
+ return core_count
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index ff8623592..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
@@ -49,6 +121,15 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
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 = ''
@@ -69,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"]
@@ -108,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']
@@ -135,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': '<'}
@@ -148,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]:
@@ -248,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/frr.py b/python/vyos/frr.py
index cbba19ab7..0ffd5cba9 100644
--- a/python/vyos/frr.py
+++ b/python/vyos/frr.py
@@ -85,7 +85,7 @@ LOG.addHandler(ch2)
_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd',
'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd',
- 'bfdd']
+ 'bfdd', 'eigrpd']
path_vtysh = '/usr/bin/vtysh'
path_frr_reload = '/usr/lib/frr/frr-reload.py'
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..33a7f9a2d 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
@@ -1498,6 +1512,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'
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/migrator.py b/python/vyos/migrator.py
index a2e0daabd..c6e3435ca 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -105,6 +105,11 @@ class Migrator(object):
sys_keys = list(sys_versions.keys())
sys_keys.sort()
+ # XXX 'bgp' needs to follow 'quagga':
+ if 'bgp' in sys_keys and 'quagga' in sys_keys:
+ sys_keys.insert(sys_keys.index('quagga'),
+ sys_keys.pop(sys_keys.index('bgp')))
+
rev_versions = {}
for key in sys_keys:
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 0b916eaae..cd15e3878 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -247,7 +247,7 @@ def load_private_key(raw_data, passphrase=None, wrap_tags=True):
if wrap_tags:
raw_data = wrap_private_key(raw_data, passphrase)
- if passphrase:
+ if passphrase is not None:
passphrase = bytes(passphrase, 'utf-8')
try:
@@ -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 de55e108b..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)
@@ -757,21 +757,26 @@ def dict_search_args(dict_object, *path):
dict_object = dict_object[item]
return dict_object
-def dict_search_recursive(dict_object, key):
+def dict_search_recursive(dict_object, key, path=[]):
""" Traverse a dictionary recurisvely and return the value of the key
we are looking for.
Thankfully copied from https://stackoverflow.com/a/19871956
+
+ Modified to yield optional path to found keys
"""
if isinstance(dict_object, list):
for i in dict_object:
- for x in dict_search_recursive(i, key):
- yield x
+ new_path = path + [i]
+ for x in dict_search_recursive(i, key, new_path):
+ yield x
elif isinstance(dict_object, dict):
if key in dict_object:
- yield dict_object[key]
- for j in dict_object.values():
- for x in dict_search_recursive(j, key):
+ new_path = path + [key]
+ yield dict_object[key], new_path
+ for k, j in dict_object.items():
+ new_path = path + [k]
+ for x in dict_search_recursive(j, key, new_path):
yield x
def get_bridge_fdb(interface):