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.py199
-rwxr-xr-xsrc/conf_mode/firewall_options.py150
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py5
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py93
-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/policy-local-route.py39
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py59
-rwxr-xr-xsrc/conf_mode/protocols_isis.py10
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py15
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py4
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py24
-rwxr-xr-xsrc/conf_mode/vrf.py20
-rwxr-xr-xsrc/conf_mode/vrf_vni.py76
16 files changed, 382 insertions, 353 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..32320a4b2 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -23,8 +23,11 @@ from ipaddress import ip_network
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.util import run
+from vyos.util import read_file
+from vyos.util import write_file
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -41,27 +44,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 +62,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 +94,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 +116,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 +150,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 +168,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}"!')
@@ -183,20 +191,19 @@ 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}')
+ if 'network_remove' in container:
+ for network in container['network_remove']:
+ call(f'podman network rm --force {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:
+ if not 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!!!
@@ -206,56 +213,82 @@ def apply(container):
tmp += ' --ipv6'
_cmd(tmp)
+ # Disable masquerading and use traditional bridging so VyOS
+ # can control firewalling/NAT by the real VyOS CLI
+ cni_network_config = f'/etc/cni/net.d/{network}.conflist'
+ tmp = read_file(cni_network_config)
+ config = json.loads(tmp)
+ if 'plugins' in config:
+ for count in range(0, len(config['plugins'])):
+ if 'ipMasq' in config['plugins'][count]:
+ config['plugins'][count]['ipMasq'] = False
+ if 'hairpinMode' in config['plugins'][count]:
+ config['plugins'][count]['hairpinMode'] = False
+
+ write_file(cni_network_config, json.dumps(config, indent=4))
+
# 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]:
+ ipparam = '--ip ' + container_config['network'][network]['address']
+ _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}')
return None
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/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 74e29ed82..6be4e918b 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
@@ -273,6 +273,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-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..f8bc073bb 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():
+ print(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/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/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 9ecfd07fe..7d05eed9f 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
@@ -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..4cf0312e9 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
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 78c1c82bd..06a29106d 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -149,14 +149,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 +186,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/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index d3065fc47..ff6090e22 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!')
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)