summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/conntrack.py11
-rwxr-xr-xsrc/conf_mode/containers.py249
-rwxr-xr-xsrc/conf_mode/dhcp_server.py43
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py15
-rwxr-xr-xsrc/conf_mode/firewall_options.py150
-rwxr-xr-xsrc/conf_mode/host_name.py49
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py74
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py7
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py93
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py34
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py11
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py1
-rwxr-xr-xsrc/conf_mode/nat.py8
-rwxr-xr-xsrc/conf_mode/nat66.py21
-rwxr-xr-xsrc/conf_mode/pki.py46
-rwxr-xr-xsrc/conf_mode/policy-local-route.py39
-rwxr-xr-xsrc/conf_mode/policy.py1
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py2
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py65
-rwxr-xr-xsrc/conf_mode/protocols_isis.py12
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py30
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py4
-rwxr-xr-xsrc/conf_mode/protocols_rip.py2
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py2
-rwxr-xr-xsrc/conf_mode/protocols_static.py2
-rwxr-xr-xsrc/conf_mode/service_webproxy.py3
-rwxr-xr-xsrc/conf_mode/system-login.py12
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py27
-rwxr-xr-xsrc/conf_mode/vrf.py20
-rwxr-xr-xsrc/conf_mode/vrf_vni.py76
-rwxr-xr-xsrc/conf_mode/vrrp.py339
31 files changed, 744 insertions, 704 deletions
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
index 4e6e39c0f..68877f794 100755
--- a/src/conf_mode/conntrack.py
+++ b/src/conf_mode/conntrack.py
@@ -97,7 +97,7 @@ def apply(conntrack):
# Depending on the enable/disable state of the ALG (Application Layer Gateway)
# modules we need to either insmod or rmmod the helpers.
for module, module_config in module_map.items():
- if dict_search(f'modules.{module}.disable', conntrack) != None:
+ if dict_search(f'modules.{module}', conntrack) is None:
if 'ko' in module_config:
for mod in module_config['ko']:
# Only remove the module if it's loaded
@@ -105,8 +105,9 @@ def apply(conntrack):
cmd(f'rmmod {mod}')
if 'iptables' in module_config:
for rule in module_config['iptables']:
- print(f'iptables --delete {rule}')
- cmd(f'iptables --delete {rule}')
+ # Only install iptables rule if it does not exist
+ tmp = run(f'iptables --check {rule}')
+ if tmp == 0: cmd(f'iptables --delete {rule}')
else:
if 'ko' in module_config:
for mod in module_config['ko']:
@@ -115,9 +116,7 @@ def apply(conntrack):
for rule in module_config['iptables']:
# Only install iptables rule if it does not exist
tmp = run(f'iptables --check {rule}')
- if tmp > 0:
- cmd(f'iptables --insert {rule}')
-
+ if tmp > 0: cmd(f'iptables --insert {rule}')
if process_named_running('conntrackd'):
# Reload conntrack-sync daemon to fetch new sysctl values
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 21b47f42a..1e0197a13 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -19,15 +19,23 @@ import json
from ipaddress import ip_address
from ipaddress import ip_network
+from time import sleep
+from json import dumps as json_write
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
+from vyos.util import call
from vyos.util import cmd
-from vyos.util import popen
-from vyos.template import render
+from vyos.util import run
+from vyos.util import read_file
+from vyos.util import write_file
+from vyos.util import is_systemd_service_active
+from vyos.util import is_systemd_service_running
+from vyos.template import inc_ip
from vyos.template import is_ipv4
from vyos.template import is_ipv6
+from vyos.template import render
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -41,27 +49,7 @@ def _cmd(command):
print(command)
return cmd(command)
-# Container management functions
-def container_exists(name):
- '''
- https://docs.podman.io/en/latest/_static/api.html#operation/ContainerExistsLibpod
- Check if container exists. Response codes.
- 204 - container exists
- 404 - no such container
- '''
- tmp = _cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/exists'")
- # If container exists it return status code "0" - code can not be displayed
- return (tmp == "")
-
-def container_status(name):
- '''
- https://docs.podman.io/en/latest/_static/api.html#operation/ContainerInspectLibpod
- '''
- tmp = _cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/json'")
- data = json.loads(tmp)
- return data['State']['Status']
-
-def ctnr_network_exists(name):
+def network_exists(name):
# Check explicit name for network, returns True if network exists
c = _cmd(f'podman network ls --quiet --filter name=^{name}$')
return bool(c)
@@ -79,11 +67,20 @@ def get_config(config=None):
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
default_values = defaults(base)
+ # container base default values can not be merged here - remove and add them later
+ if 'name' in default_values:
+ del default_values['name']
container = dict_merge(default_values, container)
+ # Merge per-container default values
+ if 'name' in container:
+ default_values = defaults(base + ['name'])
+ for name in container['name']:
+ container['name'][name] = dict_merge(default_values, container['name'][name])
+
# Delete container network, delete containers
tmp = node_changed(conf, ['container', 'network'])
- if tmp: container.update({'net_remove' : tmp})
+ if tmp: container.update({'network_remove' : tmp})
tmp = node_changed(conf, ['container', 'name'])
if tmp: container.update({'container_remove' : tmp})
@@ -102,7 +99,6 @@ def verify(container):
if len(container_config['network']) > 1:
raise ConfigError(f'Only one network can be specified for container "{name}"!')
-
# Check if the specified container network exists
network_name = list(container_config['network'])[0]
if network_name not in container['network']:
@@ -125,8 +121,25 @@ def verify(container):
# We can not use the first IP address of a network prefix as this is used by podman
if ip_address(address) == ip_network(network)[1]:
- raise ConfigError(f'Address "{address}" reserved for the container engine!')
+ raise ConfigError(f'IP address "{address}" can not be used for a container, '\
+ 'reserved for the container engine!')
+ if 'environment' in container_config:
+ for var, cfg in container_config['environment'].items():
+ if 'value' not in cfg:
+ raise ConfigError(f'Environment variable {var} has no value assigned!')
+
+ if 'volume' in container_config:
+ for volume, volume_config in container_config['volume'].items():
+ if 'source' not in volume_config:
+ raise ConfigError(f'Volume "{volume}" has no source path configured!')
+
+ if 'destination' not in volume_config:
+ raise ConfigError(f'Volume "{volume}" has no destination path configured!')
+
+ source = volume_config['source']
+ if not os.path.exists(source):
+ raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!')
# Container image is a mandatory option
if 'image' not in container_config:
@@ -142,9 +155,9 @@ def verify(container):
# Add new network
if 'network' in container:
- v4_prefix = 0
- v6_prefix = 0
for network, network_config in container['network'].items():
+ v4_prefix = 0
+ v6_prefix = 0
# If ipv4-prefix not defined for user-defined network
if 'prefix' not in network_config:
raise ConfigError(f'prefix for network "{net}" must be defined!')
@@ -160,8 +173,8 @@ def verify(container):
# A network attached to a container can not be deleted
- if {'net_remove', 'name'} <= set(container):
- for network in container['net_remove']:
+ if {'network_remove', 'name'} <= set(container):
+ for network in container['network_remove']:
for container, container_config in container['name'].items():
if 'network' in container_config and network in container_config['network']:
raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!')
@@ -173,6 +186,37 @@ def generate(container):
if not container:
return None
+ if 'network' in container:
+ for network, network_config in container['network'].items():
+ tmp = {
+ 'cniVersion' : '0.4.0',
+ 'name' : network,
+ 'plugins' : [{
+ 'type': 'bridge',
+ 'bridge': f'cni-{network}',
+ 'isGateway': True,
+ 'ipMasq': False,
+ 'hairpinMode': False,
+ 'ipam' : {
+ 'type': 'host-local',
+ 'routes': [],
+ 'ranges' : [],
+ },
+ }]
+ }
+
+ for prefix in network_config['prefix']:
+ net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}]
+ tmp['plugins'][0]['ipam']['ranges'].append(net)
+
+ # install per address-family default orutes
+ default_route = '0.0.0.0/0'
+ if is_ipv6(prefix):
+ default_route = '::/0'
+ tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route})
+
+ write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2))
+
render(config_containers_registry, 'containers/registry.tmpl', container)
render(config_containers_storage, 'containers/storage.tmpl', container)
@@ -183,79 +227,90 @@ def apply(container):
# Option "--force" allows to delete containers with any status
if 'container_remove' in container:
for name in container['container_remove']:
- if container_status(name) == 'running':
- _cmd(f'podman stop {name}')
- _cmd(f'podman rm --force {name}')
+ call(f'podman stop {name}')
+ call(f'podman rm --force {name}')
# Delete old networks if needed
- if 'net_remove' in container:
- for network in container['net_remove']:
- _cmd(f'podman network rm {network}')
-
- # Add network
- if 'network' in container:
- for network, network_config in container['network'].items():
- # Check if the network has already been created
- if not ctnr_network_exists(network) and 'prefix' in network_config:
- tmp = f'podman network create {network}'
- # we can not use list comprehension here as the --ipv6 option
- # must immediately follow the specified subnet!!!
- for prefix in sorted(network_config['prefix']):
- tmp += f' --subnet={prefix}'
- if is_ipv6(prefix):
- tmp += ' --ipv6'
- _cmd(tmp)
+ if 'network_remove' in container:
+ for network in container['network_remove']:
+ tmp = f'/etc/cni/net.d/{network}.conflist'
+ if os.path.exists(tmp):
+ os.unlink(tmp)
+
+ service_name = 'podman.service'
+ if 'network' in container or 'name' in container:
+ # Start podman if it's required and not yet running
+ if not is_systemd_service_active(service_name):
+ _cmd(f'systemctl start {service_name}')
+ # Wait for podman to be running
+ while not is_systemd_service_running(service_name):
+ sleep(0.250)
+ else:
+ _cmd(f'systemctl stop {service_name}')
# Add container
if 'name' in container:
for name, container_config in container['name'].items():
- # Check if the container has already been created
- if not container_exists(name):
- image = container_config['image']
- # Currently the best way to run a command and immediately print stdout
- print(os.system(f'podman pull {image}'))
-
- # Check/set environment options "-e foo=bar"
- env_opt = ''
- if 'environment' in container_config:
- env_opt = '-e '
- env_opt += " -e ".join(f"{k}={v['value']}" for k, v in container_config['environment'].items())
-
- # Publish ports
- port = ''
- if 'port' in container_config:
- protocol = ''
- for portmap in container_config['port']:
- if 'protocol' in container_config['port'][portmap]:
- protocol = container_config['port'][portmap]['protocol']
- protocol = f'/{protocol}'
- else:
- protocol = '/tcp'
- sport = container_config['port'][portmap]['source']
- dport = container_config['port'][portmap]['destination']
- port += f' -p {sport}:{dport}{protocol}'
-
- # Bind volume
- volume = ''
- if 'volume' in container_config:
- for vol in container_config['volume']:
- svol = container_config['volume'][vol]['source']
- dvol = container_config['volume'][vol]['destination']
- volume += f' -v {svol}:{dvol}'
-
- if 'allow_host_networks' in container_config:
- _cmd(f'podman run -dit --name {name} --net host {port} {volume} {env_opt} {image}')
- else:
- for network in container_config['network']:
- ipparam = ''
- if 'address' in container_config['network'][network]:
- ipparam = '--ip ' + container_config['network'][network]['address']
- _cmd(f'podman run --name {name} -dit --net {network} {ipparam} {port} {volume} {env_opt} {image}')
-
- # Else container is already created. Just start it.
- # It's needed after reboot.
- elif container_status(name) != 'running':
- _cmd(f'podman start {name}')
+ image = container_config['image']
+
+ if 'disable' in container_config:
+ # check if there is a container by that name running
+ tmp = _cmd('podman ps -a --format "{{.Names}}"')
+ if name in tmp:
+ _cmd(f'podman stop {name}')
+ _cmd(f'podman rm --force {name}')
+ continue
+
+ memory = container_config['memory']
+ restart = container_config['restart']
+
+ # Check if requested container image exists locally. If it does not, we
+ # pull it. print() is the best way to have a good response from the
+ # polling process to the user to display progress. If the image exists
+ # locally, a user can update it running `update container image <name>`
+ tmp = run(f'podman image exists {image}')
+ if tmp != 0: print(os.system(f'podman pull {image}'))
+
+ # Check/set environment options "-e foo=bar"
+ env_opt = ''
+ if 'environment' in container_config:
+ for k, v in container_config['environment'].items():
+ env_opt += f" -e \"{k}={v['value']}\""
+
+ # Publish ports
+ port = ''
+ if 'port' in container_config:
+ protocol = ''
+ for portmap in container_config['port']:
+ if 'protocol' in container_config['port'][portmap]:
+ protocol = container_config['port'][portmap]['protocol']
+ protocol = f'/{protocol}'
+ else:
+ protocol = '/tcp'
+ sport = container_config['port'][portmap]['source']
+ dport = container_config['port'][portmap]['destination']
+ port += f' -p {sport}:{dport}{protocol}'
+
+ # Bind volume
+ volume = ''
+ if 'volume' in container_config:
+ for vol, vol_config in container_config['volume'].items():
+ svol = vol_config['source']
+ dvol = vol_config['destination']
+ volume += f' -v {svol}:{dvol}'
+
+ container_base_cmd = f'podman run --detach --interactive --tty --replace ' \
+ f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
+ f'--name {name} {port} {volume} {env_opt}'
+ if 'allow_host_networks' in container_config:
+ _cmd(f'{container_base_cmd} --net host {image}')
+ else:
+ for network in container_config['network']:
+ ipparam = ''
+ if 'address' in container_config['network'][network]:
+ address = container_config['network'][network]['address']
+ ipparam = f'--ip {address}'
+ _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}')
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index cdee72e09..28f2a4ca5 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -148,9 +148,9 @@ def verify(dhcp):
'At least one DHCP shared network must be configured.')
# Inspect shared-network/subnet
- failover_names = []
listen_ok = False
subnets = []
+ failover_ok = False
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
@@ -159,9 +159,18 @@ def verify(dhcp):
'lease subnet must be configured.')
for subnet, subnet_config in network_config['subnet'].items():
- if 'static_route' in subnet_config and len(subnet_config['static_route']) != 2:
- raise ConfigError('Missing DHCP static-route parameter(s):\n' \
- 'destination-subnet | router must be defined!')
+ # All delivered static routes require a next-hop to be set
+ if 'static_route' in subnet_config:
+ for route, route_option in subnet_config['static_route'].items():
+ if 'next_hop' not in route_option:
+ raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+
+ # DHCP failover needs at least one subnet that uses it
+ if 'enable_failover' in subnet_config:
+ if 'failover' not in dhcp:
+ raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \
+ 'Failover is not configured globally!')
+ failover_ok = True
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
@@ -191,23 +200,6 @@ def verify(dhcp):
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
- if 'failover' in subnet_config:
- for key in ['local_address', 'peer_address', 'name', 'status']:
- if key not in subnet_config['failover']:
- raise ConfigError(f'Missing DHCP failover parameter "{key}"!')
-
- # Failover names must be uniquie
- if subnet_config['failover']['name'] in failover_names:
- name = subnet_config['failover']['name']
- raise ConfigError(f'DHCP failover names must be unique:\n' \
- f'{name} has already been configured!')
- failover_names.append(subnet_config['failover']['name'])
-
- # Failover requires start/stop ranges for pool
- if 'range' not in subnet_config:
- raise ConfigError(f'DHCP failover requires at least one start-stop range to be configured\n'\
- f'within shared-network "{network}, {subnet}" for using failover!')
-
# Exclude addresses must be in bound
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
@@ -251,6 +243,15 @@ def verify(dhcp):
if net.overlaps(net2):
raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ if 'failover' in dhcp:
+ if not failover_ok:
+ raise ConfigError('DHCP failover must be enabled for at least one subnet!')
+
+ for key in ['name', 'remote', 'source_address', 'status']:
+ if key not in dhcp['failover']:
+ tmp = key.replace('_', '-')
+ raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
+
for address in (dict_search('listen_address', dhcp) or []):
if is_addr_assigned(address):
listen_ok = True
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index c44e6c974..06366362a 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -66,21 +66,6 @@ def get_config(config=None):
if conf.exists(base_nameservers_dhcp):
dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
- # Split the source_address property into separate IPv4 and IPv6 lists
- # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed
- # as both IPv4 and IPv6 addresses can be specified in a single setting.
- source_address_v4 = []
- source_address_v6 = []
-
- for source_address in dns['source_address']:
- if is_ipv6(source_address):
- source_address_v6.append(source_address)
- else:
- source_address_v4.append(source_address)
-
- dns.update({'source_address_v4': source_address_v4})
- dns.update({'source_address_v6': source_address_v6})
-
return dns
def verify(dns):
diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py
deleted file mode 100755
index 67bf5d0e2..000000000
--- a/src/conf_mode/firewall_options.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-
-import sys
-import os
-import copy
-
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
-
-from vyos import airbag
-airbag.enable()
-
-default_config_data = {
- 'intf_opts': [],
- 'new_chain4': False,
- 'new_chain6': False
-}
-
-def get_config(config=None):
- opts = copy.deepcopy(default_config_data)
- if config:
- conf = config
- else:
- conf = Config()
- if not conf.exists('firewall options'):
- # bail out early
- return opts
- else:
- conf.set_level('firewall options')
-
- # Parse configuration of each individual instance
- if conf.exists('interface'):
- for intf in conf.list_nodes('interface'):
- conf.set_level('firewall options interface {0}'.format(intf))
- config = {
- 'intf': intf,
- 'disabled': False,
- 'mss4': '',
- 'mss6': ''
- }
-
- # Check if individual option is disabled
- if conf.exists('disable'):
- config['disabled'] = True
-
- #
- # Get MSS value IPv4
- #
- if conf.exists('adjust-mss'):
- config['mss4'] = conf.return_value('adjust-mss')
-
- # We need a marker that a new iptables chain needs to be generated
- if not opts['new_chain4']:
- opts['new_chain4'] = True
-
- #
- # Get MSS value IPv6
- #
- if conf.exists('adjust-mss6'):
- config['mss6'] = conf.return_value('adjust-mss6')
-
- # We need a marker that a new ip6tables chain needs to be generated
- if not opts['new_chain6']:
- opts['new_chain6'] = True
-
- # Append interface options to global list
- opts['intf_opts'].append(config)
-
- return opts
-
-def verify(tcp):
- # syntax verification is done via cli
- return None
-
-def apply(tcp):
- target = 'VYOS_FW_OPTIONS'
-
- # always cleanup iptables
- call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- call('iptables --table mangle --flush {} >&/dev/null'.format(target))
- call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
-
- # always cleanup ip6tables
- call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- call('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
- call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
-
- # Setup new iptables rules
- if tcp['new_chain4']:
- call('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
- call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
-
- for opts in tcp['intf_opts']:
- intf = opts['intf']
- mss = opts['mss4']
-
- # Check if this rule iis disabled
- if opts['disabled']:
- continue
-
- # adjust TCP MSS per interface
- if mss:
- call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
-
- # Setup new ip6tables rules
- if tcp['new_chain6']:
- call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
- call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
-
- for opts in tcp['intf_opts']:
- intf = opts['intf']
- mss = opts['mss6']
-
- # Check if this rule iis disabled
- if opts['disabled']:
- continue
-
- # adjust TCP MSS per interface
- if mss:
- call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
-
- return None
-
-if __name__ == '__main__':
-
- try:
- c = get_config()
- verify(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index f4c75c257..a7135911d 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -14,10 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-conf-mode script for 'system host-name' and 'system domain-name'.
-"""
-
import re
import sys
import copy
@@ -25,10 +21,13 @@ import copy
import vyos.util
import vyos.hostsd_client
-from vyos.config import Config
from vyos import ConfigError
-from vyos.util import cmd, call, process_named_running
-
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import is_ip
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import process_named_running
from vyos import airbag
airbag.enable()
@@ -37,7 +36,7 @@ default_config_data = {
'domain_name': '',
'domain_search': [],
'nameserver': [],
- 'nameservers_dhcp_interfaces': [],
+ 'nameservers_dhcp_interfaces': {},
'static_host_mapping': {}
}
@@ -51,29 +50,37 @@ def get_config(config=None):
hosts = copy.deepcopy(default_config_data)
- hosts['hostname'] = conf.return_value("system host-name")
+ hosts['hostname'] = conf.return_value(['system', 'host-name'])
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
hosts['hostname'] = default_config_data['hostname']
- if conf.exists("system domain-name"):
- hosts['domain_name'] = conf.return_value("system domain-name")
+ if conf.exists(['system', 'domain-name']):
+ hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
hosts['domain_search'].append(hosts['domain_name'])
- for search in conf.return_values("system domain-search domain"):
+ for search in conf.return_values(['system', 'domain-search', 'domain']):
hosts['domain_search'].append(search)
- hosts['nameserver'] = conf.return_values("system name-server")
+ if conf.exists(['system', 'name-server']):
+ for ns in conf.return_values(['system', 'name-server']):
+ if is_ip(ns):
+ hosts['nameserver'].append(ns)
+ else:
+ tmp = ''
+ if_type = Section.section(ns)
+ if conf.exists(['interfaces', if_type, ns, 'address']):
+ tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
- hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+ hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
# system static-host-mapping
- for hn in conf.list_nodes('system static-host-mapping host-name'):
+ for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
hosts['static_host_mapping'][hn] = {}
- hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
- hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
return hosts
@@ -103,8 +110,10 @@ def verify(hosts):
if not hostname_regex.match(a) and len(a) != 0:
raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
- # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
- # exist or doesn't have address dhcp(v6)
+ for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
+ # Warnin user if interface does not have DHCP or DHCPv6 configured
+ if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
+ print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 78c24952b..e7250fb49 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -37,6 +37,7 @@ from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -54,15 +55,17 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['interfaces', 'ethernet']
- tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ # This must be called prior to get_interface_dict(), as this function will
+ # alter the config level (config.set_level())
+ pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ base = ['interfaces', 'ethernet']
ethernet = get_interface_dict(conf, base)
if 'deleted' not in ethernet:
- ethernet['pki'] = tmp_pki
+ if pki: ethernet['pki'] = pki
return ethernet
@@ -72,12 +75,6 @@ def verify(ethernet):
ifname = ethernet['ifname']
verify_interface_exists(ifname)
-
- # No need to check speed and duplex keys as both have default values.
- if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
-
verify_mtu(ethernet)
verify_mtu_ipv6(ethernet)
verify_dhcpv6(ethernet)
@@ -86,25 +83,31 @@ def verify(ethernet):
verify_eapol(ethernet)
verify_mirror(ethernet)
- # verify offloading capabilities
- if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
- raise ConfigError('Interface does not suport RPS!')
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
- driver = EthernetIf(ifname).get_driver_name()
- # T3342 - Xen driver requires special treatment
- if driver == 'vif':
- if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
- raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
- 'for MTU size larger then 1500 bytes')
+ if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
+ # We need to verify if the requested speed and duplex setting is
+ # supported by the underlaying NIC.
+ speed = ethernet['speed']
+ duplex = ethernet['duplex']
+ if not ethtool.check_speed_duplex(speed, duplex):
+ raise ConfigError(f'Adapter does not support changing speed and duplex '\
+ f'settings to: {speed}/{duplex}!')
+
+ if 'disable_flow_control' in ethernet:
+ if not ethtool.check_flow_control():
+ raise ConfigError('Adapter does not support changing flow-control settings!')
- ethtool = Ethtool(ifname)
if 'ring_buffer' in ethernet:
- max_rx = ethtool.get_rx_buffer()
+ max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
raise ConfigError('Driver does not support RX ring-buffer configuration!')
- max_tx = ethtool.get_tx_buffer()
+ max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
raise ConfigError('Driver does not support TX ring-buffer configuration!')
@@ -118,6 +121,18 @@ def verify(ethernet):
raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
f'size of "{max_tx}" bytes!')
+ # verify offloading capabilities
+ if dict_search('offload.rps', ethernet) != None:
+ if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ raise ConfigError('Interface does not suport RPS!')
+
+ driver = ethtool.get_driver_name()
+ # T3342 - Xen driver requires special treatment
+ if driver == 'vif':
+ if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
+ raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
+ 'for MTU size larger then 1500 bytes')
+
# XDP requires multiple TX queues
if 'xdp' in ethernet:
queues = glob(f'/sys/class/net/{ifname}/queues/tx-*')
@@ -136,7 +151,7 @@ def generate(ethernet):
if 'eapol' in ethernet:
render(wpa_suppl_conf.format(**ethernet),
'ethernet/wpa_supplicant.conf.tmpl', ethernet)
-
+
ifname = ethernet['ifname']
cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
@@ -144,19 +159,16 @@ def generate(ethernet):
cert_name = ethernet['eapol']['certificate']
pki_cert = ethernet['pki']['certificate'][cert_name]
- with open(cert_file_path, 'w') as f:
- f.write(wrap_certificate(pki_cert['certificate']))
-
- with open(cert_key_path, 'w') as f:
- f.write(wrap_private_key(pki_cert['private']['key']))
+ write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
+ write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
if 'ca_certificate' in ethernet['eapol']:
ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
ca_cert_name = ethernet['eapol']['ca_certificate']
pki_ca_cert = ethernet['pki']['ca'][cert_name]
- with open(ca_cert_file_path, 'w') as f:
- f.write(wrap_certificate(pki_ca_cert['certificate']))
+ write_file(ca_cert_file_path,
+ wrap_certificate(pki_ca_cert['certificate']))
else:
# delete configuration on interface removal
if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 4bd0b22a9..2533a5b02 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -100,7 +100,7 @@ def get_config(config=None):
# need to check this first and drop those keys
if 'totp' not in tmp_openvpn['server']:
del openvpn['server']['mfa']['totp']
-
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -295,6 +295,9 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Protocol "tcp-active" is not valid in server mode')
+ if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn):
+ raise ConfigError('Cannot specify "authentication" in server mode')
+
if 'remote_port' in openvpn:
raise ConfigError('Cannot specify "remote-port" in server mode')
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 6c4c6c95b..584adc75e 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -22,12 +22,16 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_authentication
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.configverify import verify_mtu_ipv6
+from vyos.ifconfig import PPPoEIf
from vyos.template import render
from vyos.util import call
+from vyos.util import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -44,6 +48,32 @@ def get_config(config=None):
base = ['interfaces', 'pppoe']
pppoe = get_interface_dict(conf, base)
+ # We should only terminate the PPPoE session if critical parameters change.
+ # All parameters that can be changed on-the-fly (like interface description)
+ # should not lead to a reconnect!
+ tmp = leaf_node_changed(conf, ['access-concentrator'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['connect-on-demand'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['service-name'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['source-interface'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['vrf'])
+ # leaf_node_changed() returns a list, as VRF is a non-multi node, there
+ # will be only one list element
+ if tmp: pppoe.update({'vrf_old': tmp[0]})
+
+ tmp = leaf_node_changed(conf, ['authentication', 'user'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['authentication', 'password'])
+ if tmp: pppoe.update({'shutdown_required': {}})
+
return pppoe
def verify(pppoe):
@@ -66,57 +96,42 @@ def generate(pppoe):
# rendered into
ifname = pppoe['ifname']
config_pppoe = f'/etc/ppp/peers/{ifname}'
- script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{ifname}'
- script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{ifname}'
- script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{ifname}'
- script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{ifname}'
- config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
-
- config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
- script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
if 'deleted' in pppoe or 'disable' in pppoe:
- # stop DHCPv6-PD client
- call(f'systemctl stop dhcp6c@{ifname}.service')
- # Hang-up PPPoE connection
- call(f'systemctl stop ppp@{ifname}.service')
-
- # Delete PPP configuration files
- for file in config_files:
- if os.path.exists(file):
- os.unlink(file)
+ if os.path.exists(config_pppoe):
+ os.unlink(config_pppoe)
return None
# Create PPP configuration files
- render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o755)
-
- # Create script for ip-pre-up.d
- render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', pppoe,
- permission=0o755)
- # Create script for ip-up.d
- render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', pppoe,
- permission=0o755)
- # Create script for ip-down.d
- render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', pppoe,
- permission=0o755)
- # Create script for ipv6-up.d
- render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', pppoe,
- permission=0o755)
-
- if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']:
- # ipv6.tmpl relies on ifname - this should be made consitent in the
- # future better then double key-ing the same value
- render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe)
+ render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o640)
return None
def apply(pppoe):
+ ifname = pppoe['ifname']
if 'deleted' in pppoe or 'disable' in pppoe:
- call('systemctl stop ppp@{ifname}.service'.format(**pppoe))
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = PPPoEIf(ifname)
+ p.remove()
+ call(f'systemctl stop ppp@{ifname}.service')
return None
- call('systemctl restart ppp@{ifname}.service'.format(**pppoe))
+ # reconnect should only be necessary when certain config options change,
+ # like ACS name, authentication, no-peer-dns, source-interface
+ if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
+ 'shutdown_required' in pppoe):
+
+ # cleanup system (e.g. FRR routes first)
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = PPPoEIf(ifname)
+ p.remove()
+
+ call(f'systemctl restart ppp@{ifname}.service')
+ else:
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = PPPoEIf(ifname)
+ p.update(pppoe)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 294da8ef9..ef385d2e7 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from netifaces import interfaces
+from ipaddress import IPv4Address
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -94,6 +96,38 @@ def verify(tunnel):
if 'direction' not in tunnel['parameters']['erspan']:
raise ConfigError('ERSPAN version 2 requires direction to be set!')
+ # If tunnel source address any and key not set
+ if tunnel['encapsulation'] in ['gre'] and \
+ tunnel['source_address'] == '0.0.0.0' and \
+ dict_search('parameters.ip.key', tunnel) == None:
+ raise ConfigError('Tunnel parameters ip key must be set!')
+
+ if tunnel['encapsulation'] in ['gre', 'gretap']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for tunnel_if in Section.interfaces('tunnel'):
+ tunnel_cfg = get_interface_config(tunnel_if)
+ exist_encap = tunnel_cfg['linkinfo']['info_kind']
+ exist_source_address = tunnel_cfg['address']
+ exist_key = tunnel_cfg['linkinfo']['info_data']['ikey']
+ new_source_address = tunnel['source_address']
+ # Convert tunnel key to ip key, format "ip -j link show"
+ # 1 => 0.0.0.1, 999 => 0.0.3.231
+ orig_new_key = int(tunnel['parameters']['ip']['key'])
+ new_key = IPv4Address(orig_new_key)
+ new_key = str(new_key)
+ if tunnel['encapsulation'] == exist_encap and \
+ new_source_address == exist_source_address and \
+ new_key == exist_key:
+ raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ f'is already used for tunnel "{tunnel_if}"!')
+
+ # Keys are not allowed with ipip and sit tunnels
+ if tunnel['encapsulation'] in ['ipip', 'sit']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
+
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 4c566a5ad..da64dd076 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -30,6 +30,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import WireGuardIf
from vyos.util import check_kmod
+from vyos.util import check_port_availability
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -46,6 +47,9 @@ def get_config(config=None):
base = ['interfaces', 'wireguard']
wireguard = get_interface_dict(conf, base)
+ # Check if a port was changed
+ wireguard['port_changed'] = leaf_node_changed(conf, ['port'])
+
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
dict = {}
@@ -73,6 +77,13 @@ def verify(wireguard):
if 'peer' not in wireguard:
raise ConfigError('At least one Wireguard peer is required!')
+ if 'port' in wireguard and wireguard['port_changed']:
+ listen_port = int(wireguard['port'])
+ if check_port_availability('0.0.0.0', listen_port, 'udp') is not True:
+ raise ConfigError(
+ f'The UDP port {listen_port} is busy or unavailable and cannot be used for the interface'
+ )
+
# run checks on individual configured WireGuard peer
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index 31c599145..faa5eb628 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -26,7 +26,6 @@ from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
from vyos.util import dict_search
-from vyos.template import render
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index dae958774..59939d0fb 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -139,12 +139,10 @@ def verify(nat):
for rule, config in dict_search('source.rule', nat).items():
err_msg = f'Source NAT configuration error in rule {rule}:'
if 'outbound_interface' not in config:
- raise ConfigError(f'{err_msg}\n' \
- 'outbound-interface not specified')
- else:
- if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():
- print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+ if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():
+ print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
addr = dict_search('translation.address', config)
if addr != None:
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index e2bd6417d..fb376a434 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -55,7 +55,7 @@ def get_config(config=None):
conf = config
else:
conf = Config()
-
+
base = ['nat66']
nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
@@ -90,7 +90,7 @@ def get_config(config=None):
# be done only once
if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
nat['helper_functions'] = 'add'
-
+
# Retrieve current table handler positions
nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
@@ -109,21 +109,22 @@ def verify(nat):
if 'helper_functions' in nat and nat['helper_functions'] != 'has':
if not (nat['pre_ct_conntrack'] or nat['out_ct_conntrack']):
raise Exception('could not determine nftable ruleset handlers')
-
+
if dict_search('source.rule', nat):
for rule, config in dict_search('source.rule', nat).items():
err_msg = f'Source NAT66 configuration error in rule {rule}:'
if 'outbound_interface' not in config:
- raise ConfigError(f'{err_msg}\n' \
- 'outbound-interface not specified')
- else:
- if config['outbound_interface'] not in interfaces():
- print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ if config['outbound_interface'] not in interfaces():
+ raise ConfigError(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
addr = dict_search('translation.address', config)
if addr != None:
if addr != 'masquerade' and not is_ipv6(addr):
raise ConfigError(f'Warning: IPv6 address {addr} is not a valid address')
+ else:
+ raise ConfigError(f'{err_msg} translation address not specified')
prefix = dict_search('source.prefix', config)
if prefix != None:
@@ -145,7 +146,7 @@ def verify(nat):
def generate(nat):
render(iptables_nat_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755)
- render(ndppd_config, 'proxy-ndp/ndppd.conf.tmpl', nat, permission=0o755)
+ render(ndppd_config, 'ndppd/ndppd.conf.tmpl', nat, permission=0o755)
return None
def apply(nat):
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index ef1b57650..efa3578b4 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -16,8 +16,11 @@
from sys import exit
+import jmespath
+
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
from vyos.pki import load_certificate_request
@@ -26,6 +29,7 @@ from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
from vyos.util import ask_input
+from vyos.util import dict_search_recursive
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -37,14 +41,29 @@ def get_config(config=None):
else:
conf = Config()
base = ['pki']
- if not conf.exists(base):
- return None
pki = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ pki['changed'] = {}
+ tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'))
+ if tmp: pki['changed'].update({'ca' : tmp})
+
+ tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'))
+ if tmp: pki['changed'].update({'certificate' : tmp})
+
+ # We only merge on the defaults of there is a configuration at all
+ if conf.exists(base):
+ default_values = defaults(base)
+ pki = dict_merge(default_values, pki)
+
+ # We need to get the entire system configuration to verify that we are not
+ # deleting a certificate that is still referenced somewhere!
+ pki['system'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
- default_values = defaults(base)
- pki = dict_merge(default_values, pki)
return pki
def is_valid_certificate(raw_data):
@@ -142,6 +161,21 @@ def verify(pki):
if len(country) != 2 or not country.isalpha():
raise ConfigError(f'Invalid default country value. Value must be 2 alpha characters.')
+ if 'changed' in pki:
+ # if the list is getting longer, we can move to a dict() and also embed the
+ # search key as value from line 173 or 176
+ for cert_type in ['ca', 'certificate']:
+ if not cert_type in pki['changed']:
+ continue
+ for certificate in pki['changed'][cert_type]:
+ if cert_type not in pki or certificate not in pki['changed'][cert_type]:
+ if cert_type == 'ca':
+ if certificate in dict_search_recursive(pki['system'], 'ca_certificate'):
+ raise ConfigError(f'CA certificate "{certificate}" is still in use!')
+ elif cert_type == 'certificate':
+ if certificate in dict_search_recursive(pki['system'], 'certificate'):
+ raise ConfigError(f'Certificate "{certificate}" is still in use!')
+
return None
def generate(pki):
@@ -154,6 +188,8 @@ def apply(pki):
if not pki:
return None
+ # XXX: restart services if the content of a certificate changes
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 013f22665..539189442 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -44,17 +44,26 @@ def get_config(config=None):
if tmp:
for rule in (tmp or []):
src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
+ fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark'])
if src:
dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
pbr.update(dict)
+ if fwmk:
+ dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict)
+ pbr.update(dict)
# delete policy local-route rule x source x.x.x.x
+ # delete policy local-route rule x fwmark x
if 'rule' in pbr:
for rule in pbr['rule']:
src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
+ fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark'])
if src:
dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
pbr.update(dict)
+ if fwmk:
+ dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict)
+ pbr.update(dict)
return pbr
@@ -65,8 +74,8 @@ def verify(pbr):
if 'rule' in pbr:
for rule in pbr['rule']:
- if 'source' not in pbr['rule'][rule]:
- raise ConfigError('Source address required!')
+ if 'source' not in pbr['rule'][rule] and 'fwmark' not in pbr['rule'][rule]:
+ raise ConfigError('Source address or fwmark is required!')
else:
if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']:
raise ConfigError('Table set is required!')
@@ -86,16 +95,34 @@ def apply(pbr):
# Delete old rule if needed
if 'rule_remove' in pbr:
for rule in pbr['rule_remove']:
- for src in pbr['rule_remove'][rule]['source']:
- call(f'ip rule del prio {rule} from {src}')
+ if 'source' in pbr['rule_remove'][rule]:
+ for src in pbr['rule_remove'][rule]['source']:
+ call(f'ip rule del prio {rule} from {src}')
+ if 'fwmark' in pbr['rule_remove'][rule]:
+ for fwmk in pbr['rule_remove'][rule]['fwmark']:
+ call(f'ip rule del prio {rule} from all fwmark {fwmk}')
# Generate new config
if 'rule' in pbr:
for rule in pbr['rule']:
table = pbr['rule'][rule]['set']['table']
- if pbr['rule'][rule]['source']:
+ # Only source in the rule
+ # set policy local-route rule 100 source '203.0.113.1'
+ if 'source' in pbr['rule'][rule] and not 'fwmark' in pbr['rule'][rule]:
for src in pbr['rule'][rule]['source']:
call(f'ip rule add prio {rule} from {src} lookup {table}')
+ # Only fwmark in the rule
+ # set policy local-route rule 101 fwmark '23'
+ if 'fwmark' in pbr['rule'][rule] and not 'source' in pbr['rule'][rule]:
+ fwmk = pbr['rule'][rule]['fwmark']
+ call(f'ip rule add prio {rule} from all fwmark {fwmk} lookup {table}')
+ # Source and fwmark in the rule
+ # set policy local-route rule 100 source '203.0.113.1'
+ # set policy local-route rule 100 fwmark '23'
+ if 'source' in pbr['rule'][rule] and 'fwmark' in pbr['rule'][rule]:
+ fwmk = pbr['rule'][rule]['fwmark']
+ for src in pbr['rule'][rule]['source']:
+ call(f'ip rule add prio {rule} from {src} fwmark {fwmk} lookup {table}')
return None
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index d56bae9e9..1a03d520b 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -190,6 +190,7 @@ def apply(policy):
frr_cfg.modify_section(r'^bgp community-list .*')
frr_cfg.modify_section(r'^bgp extcommunity-list .*')
frr_cfg.modify_section(r'^bgp large-community-list .*')
+ frr_cfg.modify_section(r'^route-map .*')
frr_cfg.add_before('^line vty', policy['new_frr_config'])
frr_cfg.commit_configuration(bgp_daemon)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 348bae59f..539fd7b8e 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -92,7 +92,7 @@ def generate(bfd):
bfd['new_frr_config'] = ''
return None
- bfd['new_frr_config'] = render_to_string('frr/bfd.frr.tmpl', bfd)
+ bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.tmpl', bfd)
def apply(bfd):
# Save original configuration prior to starting any commit actions
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 9ecfd07fe..68284e0f9 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -23,6 +23,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_prefix_list
from vyos.configverify import verify_route_map
+from vyos.configverify import verify_vrf
from vyos.template import is_ip
from vyos.template import is_interface
from vyos.template import render_to_string
@@ -129,7 +130,7 @@ def verify(bgp):
if 'local_as' in peer_config:
if len(peer_config['local_as']) > 1:
- raise ConfigError('Only one local-as number may be specified!')
+ raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!')
# Neighbor local-as override can not be the same as the local-as
# we use for this BGP instane!
@@ -139,7 +140,7 @@ def verify(bgp):
# ttl-security and ebgp-multihop can't be used in the same configration
if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config:
- raise ConfigError('You can\'t set both ebgp-multihop and ttl-security hops')
+ raise ConfigError('You can not set both ebgp-multihop and ttl-security hops')
# Check if neighbor has both override capability and strict capability match configured at the same time.
if 'override_capability' in peer_config and 'strict_capability_match' in peer_config:
@@ -147,7 +148,7 @@ def verify(bgp):
# Check spaces in the password
if 'password' in peer_config and ' ' in peer_config['password']:
- raise ConfigError('You can\'t use spaces in the password')
+ raise ConfigError('Whitespace is not allowed in passwords!')
# Some checks can/must only be done on a neighbor and not a peer-group
if neighbor == 'neighbor':
@@ -221,27 +222,47 @@ def verify(bgp):
raise ConfigError(f'Peer-group "{peer_group}" requires remote-as to be set!')
# Throw an error if the global administrative distance parameters aren't all filled out.
- if dict_search('parameters.distance', bgp) == None:
- pass
- else:
- if dict_search('parameters.distance.global', bgp):
- for key in ['external', 'internal', 'local']:
- if dict_search(f'parameters.distance.global.{key}', bgp) == None:
- raise ConfigError('Missing mandatory configuration option for '\
- f'global administrative distance {key}!')
-
- # Throw an error if the address family specific administrative distance parameters aren't all filled out.
- if dict_search('address_family', bgp) == None:
- pass
- else:
- for address_family_name in dict_search('address_family', bgp):
- if dict_search(f'address_family.{address_family_name}.distance', bgp) == None:
- pass
- else:
+ if dict_search('parameters.distance.global', bgp) != None:
+ for key in ['external', 'internal', 'local']:
+ if dict_search(f'parameters.distance.global.{key}', bgp) == None:
+ raise ConfigError('Missing mandatory configuration option for '\
+ f'global administrative distance {key}!')
+
+ # Address Family specific validation
+ if 'address_family' in bgp:
+ for afi, afi_config in bgp['address_family'].items():
+ if 'distance' in afi_config:
+ # Throw an error if the address family specific administrative
+ # distance parameters aren't all filled out.
for key in ['external', 'internal', 'local']:
- if dict_search(f'address_family.{address_family_name}.distance.{key}', bgp) == None:
+ if key not in afi_config['distance']:
raise ConfigError('Missing mandatory configuration option for '\
- f'{address_family_name} administrative distance {key}!')
+ f'{afi} administrative distance {key}!')
+
+ if afi in ['ipv4_unicast', 'ipv6_unicast']:
+ if 'import' in afi_config and 'vrf' in afi_config['import']:
+ # Check if VRF exists
+ verify_vrf(afi_config['import']['vrf'])
+
+ # FRR error: please unconfigure vpn to vrf commands before
+ # using import vrf commands
+ if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None:
+ raise ConfigError('Please unconfigure VPN to VRF commands before '\
+ 'using "import vrf" commands!')
+
+ # Verify that the export/import route-maps do exist
+ for export_import in ['export', 'import']:
+ tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
+ if tmp: verify_route_map(tmp, bgp)
+
+ if afi in ['l2vpn_evpn'] and 'vrf' not in bgp:
+ # Some L2VPN EVPN AFI options are only supported under VRF
+ if 'vni' in afi_config:
+ for vni, vni_config in afi_config['vni'].items():
+ if 'rd' in vni_config:
+ raise ConfigError('VNI route-distinguisher is only supported under EVPN VRF')
+ if 'route_target' in vni_config:
+ raise ConfigError('VNI route-target is only supported under EVPN VRF')
return None
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index d4c82249b..4505e2496 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -113,9 +113,13 @@ def verify(isis):
# Interface MTU must be >= configured lsp-mtu
mtu = Interface(interface).get_mtu()
area_mtu = isis['lsp_mtu']
- if mtu < int(area_mtu):
- raise ConfigError(f'Interface {interface} has MTU {mtu}, minimum ' \
- f'area MTU is {area_mtu}!')
+ # Recommended maximum PDU size = interface MTU - 3 bytes
+ recom_area_mtu = mtu - 3
+ if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu:
+ raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \
+ f'current area MTU is {area_mtu}! \n' \
+ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
+ '(calculated on MTU size).')
if 'vrf' in isis:
# If interface specific options are set, we must ensure that the
@@ -144,7 +148,7 @@ def verify(isis):
exist_timers = set(required_timers).difference(set(exist_timers))
if len(exist_timers) > 0:
- raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
+ raise ConfigError('All types of spf-delay must be configured. Missing: ' + ', '.join(exist_timers).replace('_', '-'))
# If Redistribute set, but level don't set
if 'redistribute' in isis:
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 78c1c82bd..6ccda2e5a 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -87,7 +87,13 @@ def get_config(config=None):
del default_values['area']['area_type']['nssa']
if 'mpls_te' not in ospf:
del default_values['mpls_te']
- for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
+
+ for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:
+ # table is a tagNode thus we need to clean out all occurances for the
+ # default values and load them in later individually
+ if protocol == 'table':
+ del default_values['redistribute']['table']
+ continue
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
@@ -109,7 +115,6 @@ def get_config(config=None):
default_values = defaults(base + ['area', 'virtual-link'])
for area, area_config in ospf['area'].items():
if 'virtual_link' in area_config:
- print(default_values)
for virtual_link in area_config['virtual_link']:
ospf['area'][area]['virtual_link'][virtual_link] = dict_merge(
default_values, ospf['area'][area]['virtual_link'][virtual_link])
@@ -127,6 +132,12 @@ def get_config(config=None):
ospf['interface'][interface] = dict_merge(default_values,
ospf['interface'][interface])
+ if 'redistribute' in ospf and 'table' in ospf['redistribute']:
+ default_values = defaults(base + ['redistribute', 'table'])
+ for table in ospf['redistribute']['table']:
+ ospf['redistribute']['table'][table] = dict_merge(default_values,
+ ospf['redistribute']['table'][table])
+
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
#
@@ -149,14 +160,23 @@ def verify(ospf):
if route_map_name: verify_route_map(route_map_name, ospf)
if 'interface' in ospf:
- for interface in ospf['interface']:
+ for interface, interface_config in ospf['interface'].items():
verify_interface_exists(interface)
# One can not use dead-interval and hello-multiplier at the same
# time. FRR will only activate the last option set via CLI.
- if {'hello_multiplier', 'dead_interval'} <= set(ospf['interface'][interface]):
+ if {'hello_multiplier', 'dead_interval'} <= set(interface_config):
raise ConfigError(f'Can not use hello-multiplier and dead-interval ' \
f'concurrently for {interface}!')
+ # One can not use the "network <prefix> area <id>" command and an
+ # per interface area assignment at the same time. FRR will error
+ # out using: "Please remove all network commands first."
+ if 'area' in ospf and 'area' in interface_config:
+ for area, area_config in ospf['area'].items():
+ if 'network' in area_config:
+ raise ConfigError('Can not use OSPF interface area and area ' \
+ 'network configuration at the same time!')
+
if 'vrf' in ospf:
# If interface specific options are set, we must ensure that the
# interface is bound to our requesting VRF. Due to the VyOS
@@ -177,7 +197,7 @@ def generate(ospf):
ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.tmpl
ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', ospf)
- ospf['frr_ospfd_config'] = render_to_string('frr/ospf.frr.tmpl', ospf)
+ ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.tmpl', ospf)
return None
def apply(ospf):
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index fef0f509b..536ffa690 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -65,7 +65,7 @@ def verify(ospfv3):
if 'ifmtu' in if_config:
mtu = Interface(ifname).get_mtu()
if int(if_config['ifmtu']) > int(mtu):
- raise ConfigError(f'OSPFv3 ifmtu cannot go beyond physical MTU of "{mtu}"')
+ raise ConfigError(f'OSPFv3 ifmtu can not exceed physical MTU of "{mtu}"')
return None
@@ -74,7 +74,7 @@ def generate(ospfv3):
ospfv3['new_frr_config'] = ''
return None
- ospfv3['new_frr_config'] = render_to_string('frr/ospfv3.frr.tmpl', ospfv3)
+ ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.tmpl', ospfv3)
return None
def apply(ospfv3):
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index e56eb1f56..6b78f6f2d 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -93,7 +93,7 @@ def generate(rip):
rip['new_frr_config'] = ''
return None
- rip['new_frr_config'] = render_to_string('frr/rip.frr.tmpl', rip)
+ rip['new_frr_config'] = render_to_string('frr/ripd.frr.tmpl', rip)
return None
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index aaec5dacb..bc4954f63 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -95,7 +95,7 @@ def generate(ripng):
ripng['new_frr_config'] = ''
return None
- ripng['new_frr_config'] = render_to_string('frr/ripng.frr.tmpl', ripng)
+ ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.tmpl', ripng)
return None
def apply(ripng):
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 338247e30..597fcc443 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -80,7 +80,7 @@ def verify(static):
return None
def generate(static):
- static['new_frr_config'] = render_to_string('frr/static.frr.tmpl', static)
+ static['new_frr_config'] = render_to_string('frr/staticd.frr.tmpl', static)
return None
def apply(static):
diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py
index cbbd2e0bc..a16cc4aeb 100755
--- a/src/conf_mode/service_webproxy.py
+++ b/src/conf_mode/service_webproxy.py
@@ -23,6 +23,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
from vyos.util import call
+from vyos.util import chmod_755
from vyos.util import dict_search
from vyos.util import write_file
from vyos.validate import is_addr_assigned
@@ -192,6 +193,8 @@ def apply(proxy):
return None
+ if os.path.exists(squidguard_db_dir):
+ chmod_755(squidguard_db_dir)
call('systemctl restart squid.service')
return None
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index f0b92aea8..4dd7f936d 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -59,7 +59,7 @@ def get_config(config=None):
conf = Config()
base = ['system', 'login']
login = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
+ no_tag_node_value_mangle=True, get_first_key=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
@@ -80,12 +80,6 @@ def get_config(config=None):
login['radius']['server'][server] = dict_merge(default_values,
login['radius']['server'][server])
- # XXX: for a yet unknown reason when we only have one source-address
- # get_config_dict() will show a string over a string
- if 'radius' in login and 'source_address' in login['radius']:
- if isinstance(login['radius']['source_address'], str):
- login['radius']['source_address'] = [login['radius']['source_address']]
-
# create a list of all users, cli and users
all_users = list(set(local_users + cli_users))
# We will remove any normal users that dos not exist in the current
@@ -246,7 +240,9 @@ def apply(login):
# XXX: Should we deny using root at all?
home_dir = getpwnam(user).pw_dir
render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl',
- user_config, permission=0o600, user=user, group='users')
+ user_config, permission=0o600,
+ formater=lambda _: _.replace("&quot;", '"'),
+ user=user, group='users')
except Exception as e:
raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index d3065fc47..99b82ca2d 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -286,20 +286,34 @@ def verify(ipsec):
if 'pre_shared_secret' not in ra_conf['authentication']:
raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+ if 'client_mode' not in ra_conf['authentication']:
+ raise ConfigError('Client authentication method is required!')
- if 'client_mode' in ra_conf['authentication']:
- if ra_conf['authentication']['client_mode'] == 'eap-radius':
- if 'radius' not in ipsec['remote_access'] or 'server' not in ipsec['remote_access']['radius'] or len(ipsec['remote_access']['radius']['server']) == 0:
- raise ConfigError('RADIUS authentication requires at least one server')
+ if dict_search('authentication.client_mode', ra_conf) == 'eap-radius':
+ if dict_search('remote_access.radius.server', ipsec) == None:
+ raise ConfigError('RADIUS authentication requires at least one server')
if 'pool' in ra_conf:
+ if {'dhcp', 'radius'} <= set(ra_conf['pool']):
+ raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\
+ f'at the same time for "{name}"!')
+
if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1:
- raise ConfigError(f'Can not use both DHCP and a predefined address pool for "{name}"!')
+ raise ConfigError(f'Can not use DHCP and a predefined address pool for "{name}"!')
+
+ if 'radius' in ra_conf['pool'] and len(ra_conf['pool']) > 1:
+ raise ConfigError(f'Can not use RADIUS and a predefined address pool for "{name}"!')
for pool in ra_conf['pool']:
if pool == 'dhcp':
if dict_search('remote_access.dhcp.server', ipsec) == None:
raise ConfigError('IPSec DHCP server is not configured!')
+ elif pool == 'radius':
+ if dict_search('remote_access.radius.server', ipsec) == None:
+ raise ConfigError('IPSec RADIUS server is not configured!')
+
+ if dict_search('authentication.client_mode', ra_conf) != 'eap-radius':
+ raise ConfigError('RADIUS IP pool requires eap-radius client authentication!')
elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']:
raise ConfigError(f'Requested pool "{pool}" does not exist!')
@@ -348,6 +362,9 @@ def verify(ipsec):
if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
+ if {'id', 'use_x509_id'} <= set(peer_conf['authentication']):
+ raise ConfigError(f"Manually set peer id and use-x509-id are mutually exclusive!")
+
if peer_conf['authentication']['mode'] == 'x509':
if 'x509' not in peer_conf['authentication']:
raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index c1cfc1dcb..919083ac4 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -24,7 +24,6 @@ from vyos.config import Config
from vyos.configdict import node_changed
from vyos.ifconfig import Interface
from vyos.template import render
-from vyos.template import render_to_string
from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
@@ -32,12 +31,9 @@ from vyos.util import get_interface_config
from vyos.util import popen
from vyos.util import run
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'zebra'
-
config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
def list_rules():
@@ -131,7 +127,6 @@ def verify(vrf):
def generate(vrf):
render(config_file, 'vrf/vrf.conf.tmpl', vrf)
- vrf['new_frr_config'] = render_to_string('frr/vrf.frr.tmpl', vrf)
# Render nftables zones config
vrf['nft_vrf_zones'] = NamedTemporaryFile().name
render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf)
@@ -242,21 +237,6 @@ def apply(vrf):
if tmp == 0:
cmd('nft delete table inet vrf_zones')
- # T3694: Somehow we hit a priority inversion here as we need to remove the
- # VRF assigned VNI before we can remove a BGP bound VRF instance. Maybe
- # move this to an individual helper script that set's up the VNI for the
- # given VRF after any routing protocol.
- #
- # # add configuration to FRR
- # frr_cfg = frr.FRRConfig()
- # frr_cfg.load_configuration(frr_daemon)
- # frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '')
- # frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config'])
- # frr_cfg.commit_configuration(frr_daemon)
- #
- # # Save configuration to /run/frr/config/frr.conf
- # frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
new file mode 100755
index 000000000..87ee8f2d1
--- /dev/null
+++ b/src/conf_mode/vrf_vni.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-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 <http://www.gnu.org/licenses/>.
+
+from sys import argv
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'zebra'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ # This script only works with a passed VRF name
+ if len(argv) < 1:
+ raise NotImplementedError
+ vrf = argv[1]
+
+ # "assemble" dict - easier here then use a full blown get_config_dict()
+ # on a single leafNode
+ vni = { 'vrf' : vrf }
+ tmp = conf.return_value(['vrf', 'name', vrf, 'vni'])
+ if tmp: vni.update({ 'vni' : tmp })
+
+ return vni
+
+def verify(vni):
+ return None
+
+def generate(vni):
+ vni['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vni)
+ return None
+
+def apply(vni):
+ # add configuration to FRR
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '')
+ frr_cfg.add_before(r'(interface .*|line vty)', vni['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/config/frr.conf
+ frr.save_configuration()
+
+ 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/vrrp.py b/src/conf_mode/vrrp.py
index 680a80859..e8f1c1f99 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -17,244 +17,131 @@
import os
from sys import exit
-from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
-from json import dumps
-from pathlib import Path
-
-import vyos.config
-
-from vyos import ConfigError
-from vyos.util import call
-from vyos.template import render
+from ipaddress import ip_interface
+from ipaddress import IPv4Interface
+from ipaddress import IPv6Interface
+from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.ifconfig.vrrp import VRRP
-
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
def get_config(config=None):
- vrrp_groups = []
- sync_groups = []
-
if config:
- config = config
+ conf = config
else:
- config = vyos.config.Config()
-
- # Get the VRRP groups
- for group_name in config.list_nodes("high-availability vrrp group"):
- config.set_level("high-availability vrrp group {0}".format(group_name))
-
- # Retrieve the values
- group = {"preempt": True, "use_vmac": False, "disable": False}
-
- if config.exists("disable"):
- group["disable"] = True
-
- group["name"] = group_name
- group["vrid"] = config.return_value("vrid")
- group["interface"] = config.return_value("interface")
- group["description"] = config.return_value("description")
- group["advertise_interval"] = config.return_value("advertise-interval")
- group["priority"] = config.return_value("priority")
- group["hello_source"] = config.return_value("hello-source-address")
- group["peer_address"] = config.return_value("peer-address")
- group["sync_group"] = config.return_value("sync-group")
- group["preempt_delay"] = config.return_value("preempt-delay")
- group["virtual_addresses"] = config.return_values("virtual-address")
- group["virtual_addresses_excluded"] = config.return_values("virtual-address-excluded")
-
- group["auth_password"] = config.return_value("authentication password")
- group["auth_type"] = config.return_value("authentication type")
-
- group["health_check_script"] = config.return_value("health-check script")
- group["health_check_interval"] = config.return_value("health-check interval")
- group["health_check_count"] = config.return_value("health-check failure-count")
-
- group["master_script"] = config.return_value("transition-script master")
- group["backup_script"] = config.return_value("transition-script backup")
- group["fault_script"] = config.return_value("transition-script fault")
- group["stop_script"] = config.return_value("transition-script stop")
- group["script_mode_force"] = config.exists("transition-script mode-force")
-
- if config.exists("no-preempt"):
- group["preempt"] = False
- if config.exists("rfc3768-compatibility"):
- group["use_vmac"] = True
-
- # Substitute defaults where applicable
- if not group["advertise_interval"]:
- group["advertise_interval"] = 1
- if not group["priority"]:
- group["priority"] = 100
- if not group["preempt_delay"]:
- group["preempt_delay"] = 0
- if not group["health_check_interval"]:
- group["health_check_interval"] = 60
- if not group["health_check_count"]:
- group["health_check_count"] = 3
-
- # FIXUP: translate our option for auth type to keepalived's syntax
- # for simplicity
- if group["auth_type"]:
- if group["auth_type"] == "plaintext-password":
- group["auth_type"] = "PASS"
- else:
- group["auth_type"] = "AH"
-
- vrrp_groups.append(group)
-
- config.set_level("")
-
- # Get the sync group used for conntrack-sync
- conntrack_sync_group = None
- if config.exists("service conntrack-sync failover-mechanism vrrp"):
- conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group")
-
- # Get the sync groups
- for sync_group_name in config.list_nodes("high-availability vrrp sync-group"):
- config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name))
-
- sync_group = {"conntrack_sync": False}
- sync_group["name"] = sync_group_name
- sync_group["members"] = config.return_values("member")
- if conntrack_sync_group:
- if conntrack_sync_group == sync_group_name:
- sync_group["conntrack_sync"] = True
-
- # add transition script configuration
- sync_group["master_script"] = config.return_value("transition-script master")
- sync_group["backup_script"] = config.return_value("transition-script backup")
- sync_group["fault_script"] = config.return_value("transition-script fault")
- sync_group["stop_script"] = config.return_value("transition-script stop")
-
- sync_groups.append(sync_group)
-
- # create a file with dict with proposed configuration
- with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file:
- dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
-
- return (vrrp_groups, sync_groups)
-
-
-def verify(data):
- vrrp_groups, sync_groups = data
-
- for group in vrrp_groups:
- # Check required fields
- if not group["vrid"]:
- raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"]))
- if not group["interface"]:
- raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"]))
- if not group["virtual_addresses"]:
- raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"]))
-
- if group["auth_password"] and (not group["auth_type"]):
- raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"]))
-
- # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
-
- # XXX: filter on map object is destructive, so we force it to list.
- # Additionally, filter objects always evaluate to True, empty or not,
- # so we force them to lists as well.
- vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"]))
- vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
- vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
-
- if vaddrs4 and vaddrs6:
- raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"]))
-
- if vaddrs4:
- if group["hello_source"]:
- hsa = ip_address(group["hello_source"])
- if isinstance(hsa, IPv6Address):
- raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"]))
- if group["peer_address"]:
- pa = ip_address(group["peer_address"])
- if isinstance(pa, IPv6Address):
- raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"]))
-
- if vaddrs6:
- if group["hello_source"]:
- hsa = ip_address(group["hello_source"])
- if isinstance(hsa, IPv4Address):
- raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"]))
- if group["peer_address"]:
- pa = ip_address(group["peer_address"])
- if isinstance(pa, IPv4Address):
- raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"]))
-
- # Warn the user about the deprecated mode-force option
- if group['script_mode_force']:
- print("""Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.""")
- print("""It's no longer necessary, so you can safely remove it from your config now.""")
-
- # Disallow same VRID on multiple interfaces
- _groups = sorted(vrrp_groups, key=(lambda x: x["interface"]))
- count = len(_groups) - 1
- index = 0
- while (index < count):
- if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
- raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
- _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
- else:
- index += 1
-
+ conf = Config()
+
+ base = ['high-availability', 'vrrp']
+ if not conf.exists(base):
+ return None
+
+ vrrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ if 'group' in vrrp:
+ default_values = defaults(base + ['group'])
+ for group in vrrp['group']:
+ vrrp['group'][group] = dict_merge(default_values, vrrp['group'][group])
+
+ ## Get the sync group used for conntrack-sync
+ conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']
+ if conf.exists(conntrack_path):
+ vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path)
+
+ return vrrp
+
+def verify(vrrp):
+ if not vrrp:
+ return None
+
+ used_vrid_if = []
+ if 'group' in vrrp:
+ for group, group_config in vrrp['group'].items():
+ # Check required fields
+ if 'vrid' not in group_config:
+ raise ConfigError(f'VRID is required but not set in VRRP group "{group}"')
+
+ if 'interface' not in group_config:
+ raise ConfigError(f'Interface is required but not set in VRRP group "{group}"')
+
+ if 'address' not in group_config:
+ raise ConfigError(f'Virtual IP address is required but not set in VRRP group "{group}"')
+
+ if 'authentication' in group_config:
+ if not {'password', 'type'} <= set(group_config['authentication']):
+ raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
+
+ # We can not use a VRID once per interface
+ interface = group_config['interface']
+ vrid = group_config['vrid']
+ tmp = {'interface': interface, 'vrid': vrid}
+ if tmp in used_vrid_if:
+ raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!')
+ used_vrid_if.append(tmp)
+
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+
+ # XXX: filter on map object is destructive, so we force it to list.
+ # Additionally, filter objects always evaluate to True, empty or not,
+ # so we force them to lists as well.
+ vaddrs = list(map(lambda i: ip_interface(i), group_config['address']))
+ vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
+ vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
+
+ if vaddrs4 and vaddrs6:
+ raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \
+ 'Create individual groups for IPv4 and IPv6!')
+ if vaddrs4:
+ if 'hello_source_address' in group_config:
+ if is_ipv6(group_config['hello_source_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!')
+
+ if 'peer_address' in group_config:
+ if is_ipv6(group_config['peer_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!')
+
+ if vaddrs6:
+ if 'hello_source_address' in group_config:
+ if is_ipv4(group_config['hello_source_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!')
+
+ if 'peer_address' in group_config:
+ if is_ipv4(group_config['peer_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!')
# Check sync groups
- vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups))
-
- for sync_group in sync_groups:
- for m in sync_group["members"]:
- if not (m in vrrp_group_names):
- raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
-
-
-def generate(data):
- vrrp_groups, sync_groups = data
-
- # Remove disabled groups from the sync group member lists
- for sync_group in sync_groups:
- for member in sync_group["members"]:
- g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0]
- if g["disable"]:
- print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
- # Filter out disabled groups
- vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups))
-
- render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl',
- {"groups": vrrp_groups, "sync_groups": sync_groups})
- render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {})
+ if 'sync_group' in vrrp:
+ for sync_group, sync_config in vrrp['sync_group'].items():
+ if 'member' in sync_config:
+ for member in sync_config['member']:
+ if member not in vrrp['group']:
+ raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\
+ 'but it does not exist!')
+
+def generate(vrrp):
+ if not vrrp:
+ return None
+
+ render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', vrrp)
return None
+def apply(vrrp):
+ service_name = 'keepalived.service'
+ if not vrrp:
+ call(f'systemctl stop {service_name}')
+ return None
-def apply(data):
- vrrp_groups, sync_groups = data
- if vrrp_groups:
- # safely rename a temporary file with configuration dict
- try:
- dict_file = Path("{}.temp".format(VRRP.location['vyos']))
- dict_file.rename(Path(VRRP.location['vyos']))
- except Exception as err:
- print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
-
- if not VRRP.is_running():
- print("Starting the VRRP process")
- ret = call("systemctl restart keepalived.service")
- else:
- print("Reloading the VRRP process")
- ret = call("systemctl reload keepalived.service")
-
- if ret != 0:
- raise ConfigError("keepalived failed to start")
- else:
- # VRRP is removed in the commit
- print("Stopping the VRRP process")
- call("systemctl stop keepalived.service")
- os.unlink(VRRP.location['daemon'])
-
+ call(f'systemctl restart {service_name}')
return None
-
if __name__ == '__main__':
try:
c = get_config()
@@ -262,5 +149,5 @@ if __name__ == '__main__':
generate(c)
apply(c)
except ConfigError as e:
- print("VRRP error: {0}".format(str(e)))
+ print(e)
exit(1)