summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/containers.py4
-rwxr-xr-xsrc/conf_mode/https.py123
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py31
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py4
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py326
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py10
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py21
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py16
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py223
-rwxr-xr-xsrc/conf_mode/le_cert.py4
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py6
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py19
-rwxr-xr-xsrc/conf_mode/protocols_isis.py23
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py9
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py6
-rwxr-xr-xsrc/conf_mode/protocols_rip.py6
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py6
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py6
-rwxr-xr-xsrc/conf_mode/protocols_static.py6
-rwxr-xr-xsrc/conf_mode/snmp.py6
-rwxr-xr-xsrc/conf_mode/system-login.py16
-rwxr-xr-xsrc/conf_mode/system-option.py3
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py239
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py1
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py65
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py72
-rwxr-xr-xsrc/conf_mode/vrf.py58
-rwxr-xr-xsrc/conf_mode/vyos_cert.py147
-rwxr-xr-xsrc/etc/ipsec.d/vti-up-down26
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf7
-rwxr-xr-xsrc/etc/update-motd.d/99-reboot7
-rwxr-xr-xsrc/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py11
-rwxr-xr-xsrc/helpers/strip-private.py17
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py51
-rwxr-xr-xsrc/migration-scripts/https/2-to-386
-rwxr-xr-xsrc/migration-scripts/interfaces/19-to-20130
-rwxr-xr-xsrc/migration-scripts/interfaces/20-to-21130
-rwxr-xr-xsrc/migration-scripts/interfaces/22-to-23369
-rwxr-xr-xsrc/migration-scripts/ipsec/5-to-611
-rwxr-xr-xsrc/migration-scripts/l2tp/3-to-4169
-rwxr-xr-xsrc/migration-scripts/openconnect/0-to-1136
-rwxr-xr-xsrc/migration-scripts/quagga/7-to-899
-rwxr-xr-xsrc/migration-scripts/quagga/8-to-9114
-rwxr-xr-xsrc/migration-scripts/sstp/3-to-4136
-rwxr-xr-xsrc/migration-scripts/vrf/0-to-110
-rwxr-xr-xsrc/migration-scripts/vrf/1-to-21
-rwxr-xr-xsrc/migration-scripts/vrf/2-to-3144
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py2
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py230
-rwxr-xr-xsrc/op_mode/ping.py4
-rwxr-xr-xsrc/op_mode/pki.py189
-rwxr-xr-xsrc/op_mode/show_dhcp.py7
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py6
-rwxr-xr-xsrc/op_mode/show_vrf.py7
-rwxr-xr-xsrc/op_mode/wireguard.py159
-rwxr-xr-xsrc/op_mode/wireguard_client.py2
-rw-r--r--src/services/api/graphql/README.graphql116
-rw-r--r--src/services/api/graphql/graphql/__init__.py0
-rw-r--r--src/services/api/graphql/graphql/directives.py17
-rw-r--r--src/services/api/graphql/graphql/mutations.py60
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql35
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql18
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql15
-rw-r--r--src/services/api/graphql/recipes/__init__.py0
-rw-r--r--src/services/api/graphql/recipes/dhcp_server.py13
-rw-r--r--src/services/api/graphql/recipes/interface_ethernet.py13
-rw-r--r--src/services/api/graphql/recipes/recipe.py49
-rw-r--r--src/services/api/graphql/recipes/templates/dhcp_server.tmpl9
-rw-r--r--src/services/api/graphql/recipes/templates/interface_ethernet.tmpl5
-rw-r--r--src/services/api/graphql/state.py4
-rwxr-xr-xsrc/services/vyos-configd7
-rwxr-xr-xsrc/services/vyos-http-api-server27
-rw-r--r--src/systemd/isc-dhcp-server.service6
-rwxr-xr-xsrc/validators/vrf-name4
74 files changed, 2854 insertions, 1260 deletions
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 21b47f42a..7544bd840 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -142,9 +142,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!')
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index a6e2d9c8c..be4380462 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.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
@@ -14,6 +14,7 @@
# 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 os
import sys
from copy import deepcopy
@@ -23,13 +24,17 @@ import vyos.certbot_util
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import call
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
+from vyos.util import call
from vyos import airbag
airbag.enable()
config_file = '/etc/nginx/sites-available/default'
+cert_dir = '/etc/ssl/certs'
+key_dir = '/etc/ssl/private'
certbot_dir = vyos.defaults.directories['certbot']
# https config needs to coordinate several subsystems: api, certbot,
@@ -56,12 +61,58 @@ def get_config(config=None):
if not conf.exists('service https'):
return None
+ https = conf.get_config_dict('service https', get_first_key=True)
+
+ if https:
+ https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ return https
+
+def verify(https):
+ if https is None:
+ return None
+
+ if 'certificates' in https:
+ certificates = https['certificates']
+
+ if 'certificate' in certificates:
+ if not https['pki']:
+ raise ConfigError("PKI is not configured")
+
+ cert_name = certificates['certificate']
+
+ if cert_name not in https['pki']['certificate']:
+ raise ConfigError("Invalid certificate on https configuration")
+
+ pki_cert = https['pki']['certificate'][cert_name]
+
+ if 'certificate' not in pki_cert:
+ raise ConfigError("Missing certificate on https configuration")
+
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError("Missing certificate private key on https configuration")
+
+ if 'certbot' in https['certificates']:
+ vhost_names = []
+ for vh, vh_conf in https.get('virtual-host', {}).items():
+ vhost_names += vh_conf.get('server-name', [])
+ domains = https['certificates']['certbot'].get('domain-name', [])
+ domains_found = [domain for domain in domains if domain in vhost_names]
+ if not domains_found:
+ raise ConfigError("At least one 'virtual-host <id> server-name' "
+ "matching the 'certbot domain-name' is required.")
+ return None
+
+def generate(https):
+ if https is None:
+ return None
+
server_block_list = []
- https_dict = conf.get_config_dict('service https', get_first_key=True)
# organize by vhosts
- vhost_dict = https_dict.get('virtual-host', {})
+ vhost_dict = https.get('virtual-host', {})
if not vhost_dict:
# no specified virtual hosts (server blocks); use default
@@ -79,18 +130,30 @@ def get_config(config=None):
# get certificate data
- cert_dict = https_dict.get('certificates', {})
+ cert_dict = https.get('certificates', {})
+
+ if 'certificate' in cert_dict:
+ cert_name = cert_dict['certificate']
+ pki_cert = https['pki']['certificate'][cert_name]
+
+ cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
+ key_path = os.path.join(key_dir, f'{cert_name}.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
- # self-signed certificate
+ vyos_cert_data = {
+ "crt": cert_path,
+ "key": key_path
+ }
- vyos_cert_data = {}
- if 'system-generated-certificate' in list(cert_dict):
- vyos_cert_data = vyos.defaults.vyos_cert_data
- if vyos_cert_data:
for block in server_block_list:
block['vyos_cert'] = vyos_cert_data
- # letsencrypt certificate using certbot
+ # letsencrypt certificate using certbot
certbot = False
cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
@@ -110,15 +173,15 @@ def get_config(config=None):
api_set = False
api_data = {}
- if 'api' in list(https_dict):
+ if 'api' in list(https):
api_set = True
api_data = vyos.defaults.api_data
- api_settings = https_dict.get('api', {})
+ api_settings = https.get('api', {})
if api_settings:
port = api_settings.get('port', '')
if port:
api_data['port'] = port
- vhosts = https_dict.get('api-restrict', {}).get('virtual-host', [])
+ vhosts = https.get('api-restrict', {}).get('virtual-host', [])
if vhosts:
api_data['vhost'] = vhosts[:]
@@ -132,34 +195,16 @@ def get_config(config=None):
if block['id'] in vhost_list:
block['api'] = api_data
- # return dict for use in template
-
- https = {'server_block_list' : server_block_list,
- 'api_set': api_set,
- 'certbot': certbot}
-
- return https
-
-def verify(https):
- if https is None:
- return None
-
- if https['certbot']:
- for sb in https['server_block_list']:
- if sb['certbot']:
- return None
- raise ConfigError("At least one 'virtual-host <id> server-name' "
- "matching the 'certbot domain-name' is required.")
- return None
-
-def generate(https):
- if https is None:
- return None
-
if 'server_block_list' not in https or not https['server_block_list']:
https['server_block_list'] = [default_server_block]
- render(config_file, 'https/nginx.default.tmpl', https)
+ data = {
+ 'server_block_list': server_block_list,
+ 'api_set': api_set,
+ 'certbot': certbot
+ }
+
+ render(config_file, 'https/nginx.default.tmpl', data)
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 378f400b8..78c24952b 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -32,6 +32,8 @@ from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
@@ -40,6 +42,7 @@ from vyos import airbag
airbag.enable()
# XXX: wpa_supplicant works on the source interface
+cfg_dir = '/run/wpa_supplicant'
wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
def get_config(config=None):
@@ -52,8 +55,15 @@ def get_config(config=None):
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)
+
ethernet = get_interface_dict(conf, base)
+ if 'deleted' not in ethernet:
+ ethernet['pki'] = tmp_pki
+
return ethernet
def verify(ethernet):
@@ -126,6 +136,27 @@ 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')
+
+ 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']))
+
+ 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']))
else:
# delete configuration on interface removal
if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 30a27abb4..193334443 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -45,8 +45,8 @@ def generate(loopback):
return None
def apply(loopback):
- l = LoopbackIf(loopback['ifname'])
- if 'deleted' in loopback.keys():
+ l = LoopbackIf(**loopback)
+ if 'deleted' in loopback:
l.remove()
else:
l.update(loopback)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 0256ad62a..74e29ed82 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -17,6 +17,7 @@
import os
import re
+from cryptography.hazmat.primitives.asymmetric import ec
from glob import glob
from sys import exit
from ipaddress import IPv4Address
@@ -31,8 +32,14 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_diffie_hellman_length
from vyos.ifconfig import VTunIf
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_crl
+from vyos.pki import wrap_dh_parameters
+from vyos.pki import wrap_openvpn_key
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -40,6 +47,7 @@ from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import dict_search
+from vyos.util import dict_search_args
from vyos.validate import is_addr_assigned
from vyos import ConfigError
@@ -49,23 +57,9 @@ airbag.enable()
user = 'openvpn'
group = 'openvpn'
+cfg_dir = '/run/openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
-def checkCertHeader(header, filename):
- """
- Verify if filename contains specified header.
- Returns True if match is found, False if no match or file is not found
- """
- if not os.path.isfile(filename):
- return False
-
- with open(filename, 'r') as f:
- for line in f:
- if re.match(header, line):
- return True
-
- return False
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -76,14 +70,105 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'openvpn']
+
+ tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
openvpn = get_interface_dict(conf, base)
+ if 'deleted' not in openvpn:
+ openvpn['pki'] = tmp_pki
+
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
openvpn['daemon_user'] = user
openvpn['daemon_group'] = group
return openvpn
+def is_ec_private_key(pki, cert_name):
+ if not pki or 'certificate' not in pki:
+ return False
+ if cert_name not in pki['certificate']:
+ return False
+
+ pki_cert = pki['certificate'][cert_name]
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ return False
+
+ key = load_private_key(pki_cert['private']['key'])
+ return isinstance(key, ec.EllipticCurvePrivateKey)
+
+def verify_pki(openvpn):
+ pki = openvpn['pki']
+ interface = openvpn['ifname']
+ mode = openvpn['mode']
+ shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+ tls = dict_search_args(openvpn, 'tls')
+
+ if not bool(shared_secret_key) ^ bool(tls): # xor check if only one is set
+ raise ConfigError('Must specify only one of "shared-secret-key" and "tls"')
+
+ if mode in ['server', 'client'] and not tls:
+ raise ConfigError('Must specify "tls" for server and client modes')
+
+ if not pki:
+ raise ConfigError('PKI is not configured')
+
+ if shared_secret_key:
+ if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+ raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+ if shared_secret_key not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
+
+ if tls:
+ if 'ca_certificate' not in tls:
+ raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}')
+
+ if tls['ca_certificate'] not in pki['ca']:
+ raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+
+ if not (mode == 'client' and 'auth_key' in tls):
+ if 'certificate' not in tls:
+ raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}')
+
+ if 'certificate' in tls:
+ if tls['certificate'] not in pki['certificate']:
+ raise ConfigError(f'Invalid certificate on openvpn interface {interface}')
+
+ if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected'):
+ raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
+
+ if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']):
+ raise ConfigError('Must specify "tls dh-params" when not using EC keys in server mode')
+
+ if 'dh_params' in tls:
+ if 'dh' not in pki:
+ raise ConfigError('There are no DH parameters in PKI configuration')
+
+ if tls['dh_params'] not in pki['dh']:
+ raise ConfigError(f'Invalid dh-params on openvpn interface {interface}')
+
+ pki_dh = pki['dh'][tls['dh_params']]
+ dh_params = load_dh_parameters(pki_dh['parameters'])
+ dh_numbers = dh_params.parameter_numbers()
+ dh_bits = dh_numbers.p.bit_length()
+
+ if dh_bits < 2048:
+ raise ConfigError(f'Minimum DH key-size is 2048 bits')
+
+ if 'auth_key' in tls or 'crypt_key' in tls:
+ if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+ raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+ if 'auth_key' in tls:
+ if tls['auth_key'] not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid auth-key on openvpn interface {interface}')
+
+ if 'crypt_key' in tls:
+ if tls['crypt_key'] not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}')
+
def verify(openvpn):
if 'deleted' in openvpn:
verify_bridge_delete(openvpn)
@@ -108,8 +193,8 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
- if dict_search('tls.dh_file', openvpn):
- raise ConfigError('Cannot specify "tls dh-file" in client mode')
+ if dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Cannot specify "tls dh-params" in client mode')
#
# OpenVPN site-to-site - VERIFY
@@ -194,11 +279,6 @@ def verify(openvpn):
if 'remote_host' in openvpn:
raise ConfigError('Cannot specify "remote-host" in server mode')
- if 'tls' in openvpn:
- if 'dh_file' not in openvpn['tls']:
- if 'key_file' in openvpn['tls'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls']['key_file']):
- raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
-
tmp = dict_search('server.subnet', openvpn)
if tmp:
v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
@@ -306,97 +386,40 @@ def verify(openvpn):
if 'remote_host' not in openvpn:
raise ConfigError('Must specify "remote-host" with "tcp-active"')
- # shared secret and TLS
- if not ('shared_secret_key_file' in openvpn or 'tls' in openvpn):
- raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
-
- if {'shared_secret_key_file', 'tls'} <= set(openvpn):
- raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
-
- if openvpn['mode'] in ['client', 'server']:
- if 'tls' not in openvpn:
- raise ConfigError('Must specify "tls" for server and client mode')
-
#
# TLS/encryption
#
- if 'shared_secret_key_file' in openvpn:
+ if 'shared_secret_key' in openvpn:
if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
- raise ConfigError('GCM encryption with shared-secret-key-file not supported')
-
- file = dict_search('shared_secret_key_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified shared-secret-key-file "{file}" is not valid')
+ raise ConfigError('GCM encryption with shared-secret-key not supported')
if 'tls' in openvpn:
- if 'ca_cert_file' not in openvpn['tls']:
- raise ConfigError('Must specify "tls ca-cert-file"')
-
- if not (openvpn['mode'] == 'client' and 'auth_file' in openvpn['tls']):
- if 'cert_file' not in openvpn['tls']:
- raise ConfigError('Missing "tls cert-file"')
-
- if 'key_file' not in openvpn['tls']:
- raise ConfigError('Missing "tls key-file"')
-
- if {'auth_file', 'crypt_file'} <= set(openvpn['tls']):
- raise ConfigError('TLS auth and crypt are mutually exclusive')
-
- file = dict_search('tls.ca_cert_file', openvpn)
- if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
- raise ConfigError(f'Specified ca-cert-file "{file}" is invalid')
-
- file = dict_search('tls.auth_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified auth-file "{file}" is invalid')
-
- file = dict_search('tls.cert_file', openvpn)
- if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
- raise ConfigError(f'Specified cert-file "{file}" is invalid')
-
- file = dict_search('tls.key_file', openvpn)
- if file and not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', file):
- raise ConfigError(f'Specified key-file "{file}" is not valid')
-
- file = dict_search('tls.crypt_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified TLS crypt-file "{file}" is invalid')
-
- file = dict_search('tls.crl_file', openvpn)
- if file and not checkCertHeader('-----BEGIN X509 CRL-----', file):
- raise ConfigError(f'Specified crl-file "{file} not valid')
-
- file = dict_search('tls.dh_file', openvpn)
- if file and not checkCertHeader('-----BEGIN DH PARAMETERS-----', file):
- raise ConfigError(f'Specified dh-file "{file}" is not valid')
-
- if file and not verify_diffie_hellman_length(file, 2048):
- raise ConfigError(f'Minimum DH key-size is 2048 bits')
+ if {'auth_key', 'crypt_key'} <= set(openvpn['tls']):
+ raise ConfigError('TLS auth and crypt keys are mutually exclusive')
tmp = dict_search('tls.role', openvpn)
if tmp:
if openvpn['mode'] in ['client', 'server']:
- if not dict_search('tls.auth_file', openvpn):
+ if not dict_search('tls.auth_key', openvpn):
raise ConfigError('Cannot specify "tls role" in client-server mode')
if tmp == 'active':
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
- if dict_search('tls.dh_file', openvpn):
- raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
+ if dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"')
elif tmp == 'passive':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
- if not dict_search('tls.dh_file', openvpn):
- raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+ if not dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Must specify "tls dh-params" when "tls role" is "passive"')
- file = dict_search('tls.key_file', openvpn)
- if file and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', file):
- if dict_search('tls.dh_file', openvpn):
- print('Warning: using dh-file and EC keys simultaneously will ' \
+ if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
+ if 'dh_params' in openvpn['tls']:
+ print('Warning: using dh-params and EC keys simultaneously will ' \
'lead to DH ciphers being used instead of ECDH')
if dict_search('encryption.cipher', openvpn) == 'none':
@@ -404,6 +427,8 @@ def verify(openvpn):
print('No encryption will be performed and data is transmitted in ' \
'plain text over the network!')
+ verify_pki(openvpn)
+
#
# Auth user/pass
#
@@ -419,6 +444,110 @@ def verify(openvpn):
return None
+def generate_pki_files(openvpn):
+ pki = openvpn['pki']
+
+ if not pki:
+ return None
+
+ interface = openvpn['ifname']
+ shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+ tls = dict_search_args(openvpn, 'tls')
+
+ files = []
+
+ if shared_secret_key:
+ pki_key = pki['openvpn']['shared_secret'][shared_secret_key]
+ key_path = os.path.join(cfg_dir, f'{interface}_shared.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ if tls:
+ if 'ca_certificate' in tls:
+ cert_name = tls['ca_certificate']
+ pki_ca = pki['ca'][cert_name]
+
+ if 'certificate' in pki_ca:
+ cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca['certificate']))
+
+ files.append(cert_path)
+
+ if 'crl' in pki_ca:
+ for crl in pki_ca['crl']:
+ crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
+
+ with open(crl_path, 'w') as f:
+ f.write(wrap_crl(crl))
+
+ files.append(crl_path)
+ openvpn['tls']['crl'] = True
+
+ if 'certificate' in tls:
+ cert_name = tls['certificate']
+ pki_cert = pki['certificate'][cert_name]
+
+ if 'certificate' in pki_cert:
+ cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ files.append(cert_path)
+
+ if 'private' in pki_cert and 'key' in pki_cert['private']:
+ key_path = os.path.join(cfg_dir, f'{interface}_cert.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ files.append(key_path)
+ openvpn['tls']['private_key'] = True
+
+ if 'dh_params' in tls:
+ dh_name = tls['dh_params']
+ pki_dh = pki['dh'][dh_name]
+
+ if 'parameters' in pki_dh:
+ dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem')
+
+ with open(dh_path, 'w') as f:
+ f.write(wrap_dh_parameters(pki_dh['parameters']))
+
+ files.append(dh_path)
+
+ if 'auth_key' in tls:
+ key_name = tls['auth_key']
+ pki_key = pki['openvpn']['shared_secret'][key_name]
+
+ if 'key' in pki_key:
+ key_path = os.path.join(cfg_dir, f'{interface}_auth.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ if 'crypt_key' in tls:
+ key_name = tls['crypt_key']
+ pki_key = pki['openvpn']['shared_secret'][key_name]
+
+ if 'key' in pki_key:
+ key_path = os.path.join(cfg_dir, f'{interface}_crypt.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ return files
+
+
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
@@ -438,13 +567,7 @@ def generate(openvpn):
chown(ccd_dir, user, group)
# Fix file permissons for keys
- fix_permissions = []
-
- tmp = dict_search('shared_secret_key_file', openvpn)
- if tmp: fix_permissions.append(openvpn['shared_secret_key_file'])
-
- tmp = dict_search('tls.key_file', openvpn)
- if tmp: fix_permissions.append(tmp)
+ fix_permissions = generate_pki_files(openvpn)
# Generate User/Password authentication file
if 'authentication' in openvpn:
@@ -456,8 +579,9 @@ def generate(openvpn):
os.remove(openvpn['auth_user_pass_file'])
# Generate client specific configuration
- if dict_search('server.client', openvpn):
- for client, client_config in dict_search('server.client', openvpn).items():
+ server_client = dict_search_args(openvpn, 'server', 'client')
+ if server_client:
+ for client, client_config in server_client.items():
client_file = os.path.join(ccd_dir, client)
# Our client need's to know its subnet mask ...
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index 1b38304c1..57950ffea 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -45,13 +45,13 @@ def generate(vti):
return None
def apply(vti):
- if vti['ifname'] in interfaces():
- # Always delete the VTI interface in advance
+ # Remove macsec interface
+ if 'deleted' in vti:
VTIIf(**vti).remove()
+ return None
- if 'deleted' not in vti:
- tmp = VTIIf(**vti)
- tmp.update(vti)
+ tmp = VTIIf(**vti)
+ tmp.update(vti)
return None
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 8e6247a30..804f2d14f 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -25,7 +25,9 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_source_interface
-from vyos.ifconfig import VXLANIf, Interface
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VXLANIf
+from vyos.template import is_ipv6
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -65,12 +67,19 @@ def verify(vxlan):
raise ConfigError('Must configure VNI for VXLAN')
if 'source_interface' in vxlan:
- # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
- # if our configured MTU is at least 50 bytes less
+ # VXLAN adds at least an overhead of 50 byte - we need to check the
+ # underlaying device if our VXLAN package is not going to be fragmented!
+ vxlan_overhead = 50
+ if 'source_address' in vxlan and is_ipv6(vxlan['source_address']):
+ # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20
+ # bytes larger than the IPv4 header - assuming no extra options are
+ # in use.
+ vxlan_overhead += 20
+
lower_mtu = Interface(vxlan['source_interface']).get_mtu()
- if lower_mtu < (int(vxlan['mtu']) + 50):
- raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
- f'MTU is to small ({lower_mtu} bytes)')
+ if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
+ raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
+ f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 024ab8f59..4c566a5ad 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -46,17 +46,14 @@ def get_config(config=None):
base = ['interfaces', 'wireguard']
wireguard = get_interface_dict(conf, base)
- # Mangle private key - it has a default so its always valid
- wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
-
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
dict = {}
tmp = node_changed(conf, ['peer'], key_mangling=('-', '_'))
for peer in (tmp or []):
- pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
- if pubkey:
- dict = dict_merge({'peer_remove' : {peer : {'pubkey' : pubkey[0]}}}, dict)
+ public_key = leaf_node_changed(conf, ['peer', peer, 'public_key'])
+ if public_key:
+ dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict)
wireguard.update(dict)
return wireguard
@@ -70,9 +67,8 @@ def verify(wireguard):
verify_address(wireguard)
verify_vrf(wireguard)
- if not os.path.exists(wireguard['private_key']):
- raise ConfigError('Wireguard private-key not found! Execute: ' \
- '"run generate wireguard [default-keypair|named-keypairs]"')
+ if 'private_key' not in wireguard:
+ raise ConfigError('Wireguard private-key not defined')
if 'peer' not in wireguard:
raise ConfigError('At least one Wireguard peer is required!')
@@ -84,7 +80,7 @@ def verify(wireguard):
if 'allowed_ips' not in peer:
raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
- if 'pubkey' not in peer:
+ if 'public_key' not in peer:
raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
deleted file mode 100755
index a373f821f..000000000
--- a/src/conf_mode/ipsec-settings.py
+++ /dev/null
@@ -1,223 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 re
-import os
-
-from time import sleep
-from sys import exit
-
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
-from vyos.template import render
-
-from vyos import airbag
-airbag.enable()
-
-ra_conn_name = "remote-access"
-ipsec_secrets_file = "/etc/ipsec.secrets"
-ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
-ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
-ipsec_conf_file = "/etc/ipsec.conf"
-ca_cert_path = "/etc/ipsec.d/cacerts"
-server_cert_path = "/etc/ipsec.d/certs"
-server_key_path = "/etc/ipsec.d/private"
-delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
-delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
-charon_pidfile = "/var/run/charon.pid"
-
-def get_config(config=None):
- if config:
- config = config
- else:
- config = Config()
-
- if config.exists("vpn ipsec ipsec-interfaces interface"):
- data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface")
-
- # Init config variables
- data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin
- data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end
- data["ipsec_ra_conn_file"] = ipsec_ra_conn_file
- data["ra_conn_name"] = ra_conn_name
- # Get l2tp ipsec settings
- data["ipsec_l2tp"] = False
- conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful
- if config.exists(conf_ipsec_command):
- data["ipsec_l2tp"] = True
-
- # Authentication params
- if config.exists(conf_ipsec_command + "authentication mode"):
- data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode")
- if config.exists(conf_ipsec_command + "authentication pre-shared-secret"):
- data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret")
-
- # mode x509
- if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"):
- data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file")
- if config.exists(conf_ipsec_command + "authentication x509 crl-file"):
- data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file")
- if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"):
- data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")
- data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0)
- if config.exists(conf_ipsec_command + "authentication x509 server-key-file"):
- data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file")
- data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0)
- if config.exists(conf_ipsec_command + "authentication x509 server-key-password"):
- data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password")
-
- # Common l2tp ipsec params
- if config.exists(conf_ipsec_command + "ike-lifetime"):
- data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime")
- else:
- data["ipsec_l2tp_ike_lifetime"] = "3600"
-
- if config.exists(conf_ipsec_command + "lifetime"):
- data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime")
- else:
- data["ipsec_l2tp_lifetime"] = "3600"
-
- if config.exists("vpn l2tp remote-access outside-address"):
- data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address')
-
- return data
-
-def write_ipsec_secrets(c):
- if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
- secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
- elif c.get("ipsec_l2tp_auth_mode") == "x509":
- secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
-
- old_umask = os.umask(0o077)
- with open(ipsec_secrets_file, 'a+') as f:
- f.write(secret_txt)
- os.umask(old_umask)
-
-def write_ipsec_conf(c):
- ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end)
-
- old_umask = os.umask(0o077)
- with open(ipsec_conf_file, 'a+') as f:
- f.write(ipsec_confg_txt)
- os.umask(old_umask)
-
-### Remove config from file by delimiter
-def remove_confs(delim_begin, delim_end, conf_file):
- call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)
-
-
-### Checking certificate storage and notice if certificate not in /config directory
-def check_cert_file_store(cert_name, file_path, dts_path):
- if not re.search('^\/config\/.+', file_path):
- print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.")
- #Checking file existence
- if not os.path.isfile(file_path):
- raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"")
- else:
- ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
- # todo make check
- ret = call('cp -f '+file_path+' '+dts_path)
- if ret:
- raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
-
-def verify(data):
- # l2tp ipsec check
- if data["ipsec_l2tp"]:
- # Checking dependecies for "authentication mode pre-shared-secret"
- if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
- if not data.get("ipsec_l2tp_secret"):
- raise ConfigError("pre-shared-secret required")
- if not data.get("outside_addr"):
- raise ConfigError("outside-address not defined")
-
- # Checking dependecies for "authentication mode x509"
- if data.get("ipsec_l2tp_auth_mode") == "x509":
- if not data.get("ipsec_l2tp_x509_server_key_file"):
- raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.")
- else:
- check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path)
-
- if not data.get("ipsec_l2tp_x509_server_cert_file"):
- raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.")
- else:
- check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path)
-
- if not data.get("ipsec_l2tp_x509_ca_cert_file"):
- raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509")
- else:
- check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path)
-
- if not data.get('ipsec_interfaces'):
- raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")
-
-def generate(data):
- if data["ipsec_l2tp"]:
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
- # old_umask = os.umask(0o077)
- # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data)
- # os.umask(old_umask)
- ## Use this method while IPSec CLI handler won't be overwritten to python
- write_ipsec_secrets(data)
-
- old_umask = os.umask(0o077)
-
- # Create tunnels directory if does not exist
- if not os.path.exists(ipsec_ra_conn_dir):
- os.makedirs(ipsec_ra_conn_dir)
-
- render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data)
- os.umask(old_umask)
-
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
- # old_umask = os.umask(0o077)
- # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data)
- # os.umask(old_umask)
- ## Use this method while IPSec CLI handler won't be overwritten to python
- write_ipsec_conf(data)
-
- else:
- if os.path.exists(ipsec_ra_conn_file):
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
-
-def restart_ipsec():
- call('ipsec restart >&/dev/null')
- # counter for apply swanctl config
- counter = 10
- while counter <= 10:
- if os.path.exists(charon_pidfile):
- call('swanctl -q >&/dev/null')
- break
- counter -=1
- sleep(1)
- if counter == 0:
- raise ConfigError('VPN configuration error: IPSec is not running.')
-
-def apply(data):
- # Restart IPSec daemon
- restart_ipsec()
-
-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/le_cert.py b/src/conf_mode/le_cert.py
index 755c89966..6e169a3d5 100755
--- a/src/conf_mode/le_cert.py
+++ b/src/conf_mode/le_cert.py
@@ -22,6 +22,7 @@ from vyos.config import Config
from vyos import ConfigError
from vyos.util import cmd
from vyos.util import call
+from vyos.util import is_systemd_service_running
from vyos import airbag
airbag.enable()
@@ -87,8 +88,7 @@ def generate(cert):
# certbot will attempt to reload nginx, even with 'certonly';
# start nginx if not active
- ret = call('systemctl is-active --quiet nginx.service')
- if ret:
+ if not is_systemd_service_running('nginx.service'):
call('systemctl start nginx.service')
request_certbot(cert)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index dd70d6bab..348bae59f 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -102,12 +102,6 @@ def apply(bfd):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config'])
frr_cfg.commit_configuration()
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if bfd['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 95f277d74..9ecfd07fe 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -57,6 +57,11 @@ def get_config(config=None):
if not conf.exists(base):
bgp.update({'deleted' : ''})
+ if not vrf:
+ # We are running in the default VRF context, thus we can not delete
+ # our main BGP instance if there are dependent BGP VRF instances.
+ bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
+ key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
return bgp
# We also need some additional information from the config, prefix-lists
@@ -96,6 +101,11 @@ def verify_remote_as(peer_config, bgp_config):
def verify(bgp):
if not bgp or 'deleted' in bgp:
+ if 'dependent_vrfs' in bgp:
+ for vrf, vrf_options in bgp['dependent_vrfs'].items():
+ if dict_search('protocols.bgp', vrf_options) != None:
+ raise ConfigError('Cannot delete default BGP instance, ' \
+ 'dependent VRF instance(s) exist!')
return None
if 'local_as' not in bgp:
@@ -271,15 +281,6 @@ def apply(bgp):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config'])
frr_cfg.commit_configuration(bgp_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if bgp['frr_bgpd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(bgp_daemon)
- if bgp['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index c3a444f16..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
@@ -149,7 +153,7 @@ def verify(isis):
# If Redistribute set, but level don't set
if 'redistribute' in isis:
proc_level = isis.get('level','').replace('-','_')
- for afi in ['ipv4']:
+ for afi in ['ipv4', 'ipv6']:
if afi not in isis['redistribute']:
continue
@@ -198,7 +202,7 @@ def generate(isis):
isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl
isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', isis)
- isis['frr_isisd_config'] = render_to_string('frr/isis.frr.tmpl', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
return None
def apply(isis):
@@ -232,15 +236,6 @@ def apply(isis):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
frr_cfg.commit_configuration(isis_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if isis['frr_isisd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(isis_daemon)
- if isis['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 21eb8e447..78c1c82bd 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -211,15 +211,6 @@ def apply(ospf):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config'])
frr_cfg.commit_configuration(ospf_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ospf['frr_ospfd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(ospf_daemon)
- if ospf['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 1964e9d34..fef0f509b 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -86,12 +86,6 @@ def apply(ospfv3):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ospfv3['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 907ac54ac..e56eb1f56 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -117,12 +117,6 @@ def apply(rip):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config'])
frr_cfg.commit_configuration(rip_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if rip['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(rip_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index 44c080546..aaec5dacb 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -108,12 +108,6 @@ def apply(ripng):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ripng['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index d8f99efb8..947c8ab7a 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -90,12 +90,6 @@ def apply(rpki):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if rpki['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 1d45cb71c..338247e30 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -107,12 +107,6 @@ def apply(static):
frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config'])
frr_cfg.commit_configuration(static_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if static['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(static_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3990e5735..23e45a5b7 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.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
@@ -54,6 +54,7 @@ default_config_data = {
'location' : '',
'description' : '',
'contact' : '',
+ 'route_table': 'False',
'trap_source': '',
'trap_targets': [],
'vyos_user': '',
@@ -186,6 +187,9 @@ def get_config():
snmp['script_ext'].append(extension)
+ if conf.exists('oid-enable route-table'):
+ snmp['route_table'] = True
+
if conf.exists('vrf'):
# Append key to dict but don't place it in the default dictionary.
# This is required to make the override.conf.tmpl work until we
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index da0fc2a25..f0b92aea8 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -43,12 +43,11 @@ radius_config_file = "/etc/pam_radius_auth.conf"
def get_local_users():
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
- for p in getpwall():
- username = p[0]
- uid = getpwnam(username).pw_uid
+ for s_user in getpwall():
+ uid = getpwnam(s_user.pw_name).pw_uid
if uid in range(1000, 29999):
- if username not in ['radius_user', 'radius_priv_user']:
- local_users.append(username)
+ if s_user.pw_name not in ['radius_user', 'radius_priv_user']:
+ local_users.append(s_user.pw_name)
return local_users
@@ -104,7 +103,14 @@ def verify(login):
raise ConfigError(f'Attempting to delete current user: {cur_user}')
if 'user' in login:
+ system_users = getpwall()
for user, user_config in login['user'].items():
+ # Linux system users range up until UID 1000, we can not create a
+ # VyOS CLI user which already exists as system user
+ for s_user in system_users:
+ if s_user.pw_name == user and s_user.pw_uid < 1000:
+ raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index 454611c55..55cf6b142 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.py
@@ -24,6 +24,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
from vyos.util import cmd
+from vyos.util import is_systemd_service_running
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
from vyos import ConfigError
@@ -114,7 +115,7 @@ def apply(options):
if 'performance' in options:
cmd('systemctl restart tuned.service')
# wait until daemon has started before sending configuration
- while (int(os.system('systemctl is-active --quiet tuned.service')) != 0):
+ while (not is_systemd_service_running('tuned.service')):
sleep(0.250)
cmd('tuned-adm profile network-{performance}'.format(**options))
else:
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 3fab8e868..d3065fc47 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -33,9 +33,12 @@ from vyos.pki import wrap_crl
from vyos.pki import wrap_public_key
from vyos.pki import wrap_private_key
from vyos.template import ip_from_cidr
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
from vyos.template import render
from vyos.validate import is_ipv6_link_local
from vyos.util import call
+from vyos.util import dict_search
from vyos.util import dict_search_args
from vyos.util import run
from vyos.xml import defaults
@@ -46,13 +49,14 @@ airbag.enable()
dhcp_wait_attempts = 2
dhcp_wait_sleep = 1
-swanctl_dir = '/etc/swanctl'
-ipsec_conf = '/etc/ipsec.conf'
-ipsec_secrets = '/etc/ipsec.secrets'
-charon_conf = '/etc/strongswan.d/charon.conf'
-charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
-interface_conf = '/etc/strongswan.d/interfaces_use.conf'
-swanctl_conf = f'{swanctl_dir}/swanctl.conf'
+swanctl_dir = '/etc/swanctl'
+ipsec_conf = '/etc/ipsec.conf'
+ipsec_secrets = '/etc/ipsec.secrets'
+charon_conf = '/etc/strongswan.d/charon.conf'
+charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
+charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+interface_conf = '/etc/strongswan.d/interfaces_use.conf'
+swanctl_conf = f'{swanctl_dir}/swanctl.conf'
default_install_routes = 'yes'
@@ -73,6 +77,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['vpn', 'ipsec']
+ l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
if not conf.exists(base):
return None
@@ -97,26 +102,51 @@ def get_config(config=None):
ipsec['esp_group'][group])
if 'ike_group' in ipsec:
default_values = defaults(base + ['ike-group'])
+ # proposal is a tag node which may come with individual defaults per node
+ if 'proposal' in default_values:
+ del default_values['proposal']
+
for group in ipsec['ike_group']:
ipsec['ike_group'][group] = dict_merge(default_values,
ipsec['ike_group'][group])
- if 'remote_access' in ipsec:
- default_values = defaults(base + ['remote-access'])
- for rw in ipsec['remote_access']:
- ipsec['remote_access'][rw] = dict_merge(default_values,
- ipsec['remote_access'][rw])
+
+ if 'proposal' in ipsec['ike_group'][group]:
+ default_values = defaults(base + ['ike-group', 'proposal'])
+ for proposal in ipsec['ike_group'][group]['proposal']:
+ ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values,
+ ipsec['ike_group'][group]['proposal'][proposal])
+
+ if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
+ default_values = defaults(base + ['remote-access', 'connection'])
+ for rw in ipsec['remote_access']['connection']:
+ ipsec['remote_access']['connection'][rw] = dict_merge(default_values,
+ ipsec['remote_access']['connection'][rw])
+
+ if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
+ default_values = defaults(base + ['remote-access', 'radius', 'server'])
+ for server in ipsec['remote_access']['radius']['server']:
+ ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values,
+ ipsec['remote_access']['radius']['server'][server])
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
- ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces',
- 'interface'])
- ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access',
- 'ipsec-settings'])
+ ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True)
+ tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ if tmp:
+ ipsec['l2tp'] = tmp
+ l2tp_defaults = defaults(l2tp_base)
+ ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp'])
+ ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address'])
+ ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
+ ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
+
return ipsec
def get_dhcp_address(iface):
@@ -165,14 +195,43 @@ def verify(ipsec):
if not ipsec:
return None
- if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']:
- interfaces = ipsec['ipsec_interfaces']['interface']
- if isinstance(interfaces, str):
- interfaces = [interfaces]
-
- for ifname in interfaces:
+ if 'interfaces' in ipsec :
+ for ifname in ipsec['interface']:
verify_interface_exists(ifname)
+ if 'l2tp' in ipsec:
+ if 'esp_group' in ipsec['l2tp']:
+ if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on L2TP remote-access config")
+
+ if 'ike_group' in ipsec['l2tp']:
+ if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on L2TP remote-access config")
+
+ if 'authentication' not in ipsec['l2tp']:
+ raise ConfigError(f'Missing authentication settings on L2TP remote-access config')
+
+ if 'mode' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing authentication mode on L2TP remote-access config')
+
+ if not ipsec['l2tp_outside_address']:
+ raise ConfigError(f'Missing outside-address on L2TP remote-access config')
+
+ if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret':
+ if 'pre_shared_secret' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing pre shared secret on L2TP remote-access config')
+
+ if ipsec['l2tp']['authentication']['mode'] == 'x509':
+ if 'x509' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing x509 settings on L2TP remote-access config')
+
+ x509 = ipsec['l2tp']['authentication']['x509']
+
+ if 'ca_certificate' not in x509 or 'certificate' not in x509:
+ raise ConfigError(f'Missing x509 certificates on L2TP remote-access config')
+
+ verify_pki_x509(ipsec['pki'], x509)
+
if 'profile' in ipsec:
for profile, profile_conf in ipsec['profile'].items():
if 'esp_group' in profile_conf:
@@ -191,35 +250,86 @@ def verify(ipsec):
raise ConfigError(f"Missing authentication on {profile} profile")
if 'remote_access' in ipsec:
- for name, ra_conf in ipsec['remote_access'].items():
- if 'esp_group' in ra_conf:
- if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
- raise ConfigError(f"Invalid esp-group on {name} remote-access config")
- else:
- raise ConfigError(f"Missing esp-group on {name} remote-access config")
-
- if 'ike_group' in ra_conf:
- if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
- raise ConfigError(f"Invalid ike-group on {name} remote-access config")
- else:
- raise ConfigError(f"Missing ike-group on {name} remote-access config")
-
- if 'authentication' not in ra_conf:
- raise ConfigError(f"Missing authentication on {name} remote-access config")
-
- if ra_conf['authentication']['server_mode'] == 'x509':
- if 'x509' not in ra_conf['authentication']:
- raise ConfigError(f"Missing x509 settings on {name} remote-access config")
-
- x509 = ra_conf['authentication']['x509']
-
- if 'ca_certificate' not in x509 or 'certificate' not in x509:
- raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
-
- verify_pki_x509(ipsec['pki'], x509)
- elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
- if 'pre_shared_secret' not in ra_conf['authentication']:
- raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+ if 'connection' in ipsec['remote_access']:
+ for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'esp_group' in ra_conf:
+ if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on {name} remote-access config")
+ else:
+ raise ConfigError(f"Missing esp-group on {name} remote-access config")
+
+ if 'ike_group' in ra_conf:
+ if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on {name} remote-access config")
+
+ ike = ra_conf['ike_group']
+ if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
+ raise ConfigError('IPSec remote-access connections requires IKEv2!')
+
+ else:
+ raise ConfigError(f"Missing ike-group on {name} remote-access config")
+
+ if 'authentication' not in ra_conf:
+ raise ConfigError(f"Missing authentication on {name} remote-access config")
+
+ if ra_conf['authentication']['server_mode'] == 'x509':
+ if 'x509' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing x509 settings on {name} remote-access config")
+
+ x509 = ra_conf['authentication']['x509']
+
+ if 'ca_certificate' not in x509 or 'certificate' not in x509:
+ raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
+
+ verify_pki_x509(ipsec['pki'], x509)
+ elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
+ if 'pre_shared_secret' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+
+
+ 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 'pool' in ra_conf:
+ 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}"!')
+
+ 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' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']:
+ raise ConfigError(f'Requested pool "{pool}" does not exist!')
+
+ if 'pool' in ipsec['remote_access']:
+ for pool, pool_config in ipsec['remote_access']['pool'].items():
+ if 'prefix' not in pool_config:
+ raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
+
+ if 'name_server' in pool_config:
+ if len(pool_config['name_server']) > 2:
+ raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
+
+ for ns in pool_config['name_server']:
+ v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
+ v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
+ if v4_addr_and_ns or v6_addr_and_ns:
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
+
+ if 'exclude' in pool_config:
+ for exclude in pool_config['exclude']:
+ v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
+ v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
+ if v4_addr_and_exclude or v6_addr_and_exclude:
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
+
+ if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
+ for server, server_config in ipsec['remote_access']['radius']['server'].items():
+ if 'key' not in server_config:
+ raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
for peer, peer_conf in ipsec['site_to_site']['peer'].items():
@@ -372,7 +482,8 @@ def generate(ipsec):
cleanup_pki_files()
if not ipsec:
- for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]:
+ for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf,
+ charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes})
@@ -389,8 +500,13 @@ def generate(ipsec):
if not os.path.exists(KEY_PATH):
os.mkdir(KEY_PATH, mode=0o700)
- if 'remote_access' in ipsec:
- for rw, rw_conf in ipsec['remote_access'].items():
+ if 'l2tp' in ipsec:
+ if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']:
+ generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509'])
+
+ if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
+ for rw, rw_conf in ipsec['remote_access']['connection'].items():
+
if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']:
generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509'])
@@ -436,17 +552,10 @@ def generate(ipsec):
render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec)
render(charon_conf, 'ipsec/charon.tmpl', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec)
+ render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec)
-def resync_l2tp(ipsec):
- if ipsec and not ipsec['l2tp_exists']:
- return
-
- tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py')
- if tmp > 0:
- print('ERROR: failed to reapply L2TP IPSec settings!')
-
def resync_nhrp(ipsec):
if ipsec and not ipsec['nhrp_exists']:
return
@@ -470,17 +579,13 @@ def apply(ipsec):
if not ipsec:
call('sudo ipsec stop')
else:
- args = ''
- if 'auto_update' in ipsec:
- args = '--auto-update ' + ipsec['auto_update']
- call(f'sudo ipsec restart {args}')
+ call('sudo ipsec restart')
call('sudo ipsec rereadall')
call('sudo ipsec reload')
if wait_for_vici_socket():
call('sudo swanctl -q')
- resync_l2tp(ipsec)
resync_nhrp(ipsec)
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index e970d2ef5..9c52f77ca 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -20,7 +20,6 @@ import re
from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from sys import exit
-from time import sleep
from ipaddress import ip_network
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 2986c3458..f6db196dc 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -19,9 +19,11 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
-from vyos.xml import defaults
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.xml import defaults
from vyos import ConfigError
from crypt import crypt, mksalt, METHOD_SHA512
@@ -50,6 +52,10 @@ def get_config():
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
+ if ocserv:
+ ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
return ocserv
def verify(ocserv):
@@ -72,14 +78,36 @@ def verify(ocserv):
raise ConfigError('openconnect authentication credentials required')
# Check ssl
- if "ssl" in ocserv:
- req_cert = ['cert_file', 'key_file']
- for cert in req_cert:
- if not cert in ocserv["ssl"]:
- raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-')))
- else:
+ if 'ssl' not in ocserv:
raise ConfigError('openconnect ssl required')
+ if not ocserv['pki'] or 'certificate' not in ocserv['pki']:
+ raise ConfigError('PKI not configured')
+
+ ssl = ocserv['ssl']
+ if 'certificate' not in ssl:
+ raise ConfigError('openconnect ssl certificate required')
+
+ cert_name = ssl['certificate']
+
+ if cert_name not in ocserv['pki']['certificate']:
+ raise ConfigError('Invalid openconnect ssl certificate')
+
+ cert = ocserv['pki']['certificate'][cert_name]
+
+ if 'certificate' not in cert:
+ raise ConfigError('Missing certificate in PKI')
+
+ if 'private' not in cert or 'key' not in cert['private']:
+ raise ConfigError('Missing private key in PKI')
+
+ if 'ca_certificate' in ssl:
+ if 'ca' not in ocserv['pki']:
+ raise ConfigError('PKI not configured')
+
+ if ssl['ca_certificate'] not in ocserv['pki']['ca']:
+ raise ConfigError('Invalid openconnect ssl CA certificate')
+
# Check network settings
if "network_settings" in ocserv:
if "push_route" in ocserv["network_settings"]:
@@ -109,6 +137,29 @@ def generate(ocserv):
# Render local users
render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"])
+ if "ssl" in ocserv:
+ cert_file_path = os.path.join(cfg_dir, 'cert.pem')
+ cert_key_path = os.path.join(cfg_dir, 'cert.key')
+ ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem')
+
+ if 'certificate' in ocserv['ssl']:
+ cert_name = ocserv['ssl']['certificate']
+ pki_cert = ocserv['pki']['certificate'][cert_name]
+
+ with open(cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ if 'private' in pki_cert and 'key' in pki_cert['private']:
+ with open(cert_key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ if 'ca_certificate' in ocserv['ssl']:
+ ca_name = ocserv['ssl']['ca_certificate']
+ pki_ca_cert = ocserv['pki']['ca'][ca_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca_cert['certificate']))
+
# Render config
render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv)
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 47367f125..d1a71a5ad 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -21,6 +21,8 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
from vyos.configverify import verify_accel_ppp_base_service
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
@@ -28,6 +30,7 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
+cfg_dir = '/run/accel-pppd'
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -42,6 +45,11 @@ def get_config(config=None):
# retrieve common dictionary keys
sstp = get_accel_dict(conf, base, sstp_chap_secrets)
+
+ if sstp:
+ sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
return sstp
def verify(sstp):
@@ -56,31 +64,59 @@ def verify(sstp):
#
# SSL certificate checks
#
- tmp = dict_search('ssl.ca_cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL CA certificate file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!')
+ if not sstp['pki']:
+ raise ConfigError('PKI is not configured')
- tmp = dict_search('ssl.cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL public key file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL public key "{tmp}" does not exist!')
+ if 'ssl' not in sstp:
+ raise ConfigError('SSL missing on SSTP config')
- tmp = dict_search('ssl.key_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL private key file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL private key "{tmp}" does not exist!')
+ ssl = sstp['ssl']
+
+ if 'ca_certificate' not in ssl:
+ raise ConfigError('SSL CA certificate missing on SSTP config')
+
+ if 'certificate' not in ssl:
+ raise ConfigError('SSL certificate missing on SSTP config')
+
+ cert_name = ssl['certificate']
+
+ if ssl['ca_certificate'] not in sstp['pki']['ca']:
+ raise ConfigError('Invalid CA certificate on SSTP config')
+
+ if cert_name not in sstp['pki']['certificate']:
+ raise ConfigError('Invalid certificate on SSTP config')
+
+ pki_cert = sstp['pki']['certificate'][cert_name]
+
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError('Missing private key for certificate on SSTP config')
+
+ if 'password_protected' in pki_cert['private']:
+ raise ConfigError('Encrypted private key is not supported on SSTP config')
def generate(sstp):
if not sstp:
return None
+ cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem')
+ cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')
+ ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem')
+
+ cert_name = sstp['ssl']['certificate']
+ pki_cert = sstp['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']))
+
+ ca_cert_name = sstp['ssl']['ca_certificate']
+ pki_ca = sstp['pki']['ca'][ca_cert_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca['certificate']))
+
# accel-cmd reload doesn't work so any change results in a restart of the daemon
render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 936561edc..c1cfc1dcb 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from json import loads
+from tempfile import NamedTemporaryFile
from vyos.config import Config
from vyos.configdict import node_changed
@@ -28,6 +29,8 @@ from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
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
@@ -125,11 +128,17 @@ def verify(vrf):
return None
+
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)
+
return None
+
def apply(vrf):
# Documentation
#
@@ -151,8 +160,19 @@ def apply(vrf):
call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip link delete dev {tmp}')
+ # Remove nftables conntrack zone map item
+ nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
+ cmd(f'nft {nft_del_element}')
if 'name' in vrf:
+ # Separate VRFs in conntrack table
+ # check if table already exists
+ _, err = popen('nft list table inet vrf_zones')
+ # If not, create a table
+ if err:
+ cmd(f'nft -f {vrf["nft_vrf_zones"]}')
+ os.unlink(vrf['nft_vrf_zones'])
+
for name, config in vrf['name'].items():
table = config['table']
@@ -182,6 +202,9 @@ def apply(vrf):
# reconfiguration.
state = 'down' if 'disable' in config else 'up'
vrf_if.set_admin_state(state)
+ # Add nftables conntrack zone map item
+ nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
+ cmd(f'nft {nft_add_element}')
# Linux routing uses rules to find tables - routing targets are then
# looked up in those tables. If the lookup got a matching route, the
@@ -214,22 +237,25 @@ def apply(vrf):
# clean out l3mdev-table rule if present
if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
call(f'ip {af} rule del pref 1000')
-
- # 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)
-
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if vrf['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ # Remove VRF zones table from nftables
+ tmp = run('nft list table inet vrf_zones')
+ 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
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
deleted file mode 100755
index dc7c64684..000000000
--- a/src/conf_mode/vyos_cert.py
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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 tempfile
-import pathlib
-import ssl
-
-import vyos.defaults
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import cmd
-
-from vyos import airbag
-airbag.enable()
-
-vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
-
-# XXX: this model will need to be extended for tag nodes
-dependencies = [
- 'https.py',
-]
-
-def status_self_signed(cert_data):
-# check existence and expiration date
- path = pathlib.Path(cert_data['conf'])
- if not path.is_file():
- return False
- path = pathlib.Path(cert_data['crt'])
- if not path.is_file():
- return False
- path = pathlib.Path(cert_data['key'])
- if not path.is_file():
- return False
-
- # check if certificate is 1/2 past lifetime, with openssl -checkend
- end_days = int(cert_data['lifetime'])
- end_seconds = int(0.5*60*60*24*end_days)
- checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data)
- try:
- cmd(checkend_cmd, message='Called process error')
- return True
- except OSError as err:
- if err.errno == 1:
- return False
- print(err)
- # XXX: This seems wrong to continue on failure
- # implicitely returning None
-
-def generate_self_signed(cert_data):
- san_config = None
-
- if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
- san_config = tempfile.NamedTemporaryFile()
- with open(san_config.name, 'w') as fd:
- fd.write('[req]\n')
- fd.write('distinguished_name=req\n')
- fd.write('[san]\n')
- fd.write('subjectAltName=DNS:vyos\n')
-
- openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
- '-newkey rsa:4096 -keyout {key} -out {crt} '
- '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
- '-extensions san -config {san_conf}'
- ''.format(san_conf=san_config.name,
- **cert_data))
-
- else:
- openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
- '-newkey rsa:4096 -keyout {key} -out {crt} '
- '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
- '-addext "subjectAltName=DNS:vyos"'
- ''.format(**cert_data))
-
- try:
- cmd(openssl_req_cmd, message='Called process error')
- except OSError as err:
- print(err)
- # XXX: seems wrong to ignore the failure
-
- os.chmod('{key}'.format(**cert_data), 0o400)
-
- with open('{conf}'.format(**cert_data), 'w') as f:
- f.write('ssl_certificate {crt};\n'.format(**cert_data))
- f.write('ssl_certificate_key {key};\n'.format(**cert_data))
-
- if san_config:
- san_config.close()
-
-def get_config(config=None):
- vyos_cert = vyos.defaults.vyos_cert_data
-
- if config:
- conf = config
- else:
- conf = Config()
- if not conf.exists('service https certificates system-generated-certificate'):
- return None
- else:
- conf.set_level('service https certificates system-generated-certificate')
-
- if conf.exists('lifetime'):
- lifetime = conf.return_value('lifetime')
- vyos_cert['lifetime'] = lifetime
-
- return vyos_cert
-
-def verify(vyos_cert):
- return None
-
-def generate(vyos_cert):
- if vyos_cert is None:
- return None
-
- if not status_self_signed(vyos_cert):
- generate_self_signed(vyos_cert)
-
-def apply(vyos_cert):
- for dep in dependencies:
- command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- cmd(command, raising=ConfigError)
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down
index 2b66dd9e6..281c9bf2b 100755
--- a/src/etc/ipsec.d/vti-up-down
+++ b/src/etc/ipsec.d/vti-up-down
@@ -19,7 +19,15 @@
import os
import sys
-from vyos.util import call, get_interface_config, get_interface_address
+from syslog import syslog
+from syslog import openlog
+from syslog import LOG_PID
+from syslog import LOG_INFO
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import call
+from vyos.util import get_interface_config
+from vyos.util import get_interface_address
def get_dhcp_address(interface):
addr = get_interface_address(interface)
@@ -35,7 +43,8 @@ if __name__ == '__main__':
interface = sys.argv[1]
dhcp_interface = sys.argv[2]
- print(f'vti-up-down: start: {verb} {connection} {interface}')
+ openlog(ident=f'vti-up-down', logoption=LOG_PID, facility=LOG_INFO)
+ syslog(f'Interface {interface} {verb} {connection}')
if verb in ['up-client', 'up-host']:
call('sudo ip route delete default table 220')
@@ -43,19 +52,24 @@ if __name__ == '__main__':
vti_link = get_interface_config(interface)
if not vti_link:
- print('vti-up-down: interface not found')
+ syslog(f'Interface {interface} not found')
sys.exit(0)
vti_link_up = (vti_link['operstate'] == 'UP' if 'operstate' in vti_link else False)
+ config = ConfigTreeQuery()
+ vti_dict = config.get_config_dict(['interfaces', 'vti', interface],
+ get_first_key=True)
+
if verb in ['up-client', 'up-host']:
if not vti_link_up:
if dhcp_interface != 'no':
local_ip = get_dhcp_address(dhcp_interface)
call(f'sudo ip tunnel change {interface} local {local_ip}')
- call(f'sudo ip link set {interface} up')
+ if 'disable' not in vti_dict:
+ call(f'sudo ip link set {interface} up')
+ else:
+ syslog(f'Interface {interface} is admin down ...')
elif verb in ['down-client', 'down-host']:
if vti_link_up:
call(f'sudo ip link set {interface} down')
-
- print('vti-up-down: finish') \ No newline at end of file
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index 8265e12dc..e03d3a29c 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -72,6 +72,12 @@ net.ipv4.conf.default.send_redirects=1
# Increase size of buffer for netlink
net.core.rmem_max=2097152
+# Remove IPv4 and IPv6 routes from forward information base when link goes down
+net.ipv4.conf.all.ignore_routes_with_linkdown=1
+net.ipv4.conf.default.ignore_routes_with_linkdown=1
+net.ipv6.conf.all.ignore_routes_with_linkdown=1
+net.ipv6.conf.default.ignore_routes_with_linkdown=1
+
# Enable packet forwarding for IPv6
net.ipv6.conf.all.forwarding=1
@@ -81,6 +87,7 @@ net.ipv6.route.max_size = 262144
# Do not forget IPv6 addresses when a link goes down
net.ipv6.conf.default.keep_addr_on_down=1
net.ipv6.conf.all.keep_addr_on_down=1
+net.ipv6.route.skip_notify_on_dev_down=1
# Default value of 20 seems to interfere with larger OSPF and VRRP setups
net.ipv4.igmp_max_memberships = 512
diff --git a/src/etc/update-motd.d/99-reboot b/src/etc/update-motd.d/99-reboot
new file mode 100755
index 000000000..718be1a7a
--- /dev/null
+++ b/src/etc/update-motd.d/99-reboot
@@ -0,0 +1,7 @@
+#!/bin/vbash
+source /opt/vyatta/etc/functions/script-template
+if [ -f /run/systemd/shutdown/scheduled ]; then
+ echo
+ run show reboot
+fi
+exit
diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
index ec33906ba..4e7fb117c 100755
--- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
+++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
@@ -25,9 +25,8 @@ def get_config():
c = Config()
interfaces = dict()
for intf in c.list_effective_nodes('interfaces ethernet'):
- # skip interfaces that are disabled or is configured for dhcp
+ # skip interfaces that are disabled
check_disable = f'interfaces ethernet {intf} disable'
- check_dhcp = f'interfaces ethernet {intf} address dhcp'
if c.exists_effective(check_disable):
continue
@@ -49,10 +48,10 @@ def apply(config):
# add configured addresses to interface
for addr in addresses:
- if addr == 'dhcp':
- cmd = ['dhclient', intf]
- else:
- cmd = f'ip address add {addr} dev {intf}'
+ # dhcp is handled by netplug
+ if addr in ['dhcp', 'dhcpv6']:
+ continue
+ cmd = f'ip address add {addr} dev {intf}'
syslog.syslog(cmd)
run(cmd)
diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py
index 420a039eb..c165d2cba 100755
--- a/src/helpers/strip-private.py
+++ b/src/helpers/strip-private.py
@@ -116,32 +116,33 @@ if __name__ == "__main__":
(True, re.compile(r'pre-shared-secret \S+'), 'pre-shared-secret xxxxxx'),
# Strip OSPF md5-key
(True, re.compile(r'md5-key \S+'), 'md5-key xxxxxx'),
-
+ # Strip WireGuard private-key
+ (True, re.compile(r'private-key \S+'), 'private-key xxxxxx'),
+
# Strip MAC addresses
(args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'XX:XX:XX:XX:XX:\2'),
# Strip host-name, domain-name, and domain-search
(args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'),
-
+
# Strip user-names
(args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'),
# Strip full-name
(args.username, re.compile(r'(full-name) [ -_A-Z a-z]+'), r'\1 xxxxxx'),
-
+
# Strip DHCP static-mapping and shared network names
(args.dhcp, re.compile(r'(shared-network-name|static-mapping) \S+'), r'\1 xxxxxx'),
-
+
# Strip host/domain names
(args.domain, re.compile(r' (peer|remote-host|local-host|server) ([\w-]+\.)+[\w-]+'), r' \1 xxxxx.tld'),
-
+
# Strip BGP ASNs
(args.asn, re.compile(r'(bgp|remote-as) (\d+)'), r'\1 XXXXXX'),
-
+
# Strip LLDP location parameters
(args.lldp, re.compile(r'(altitude|datum|latitude|longitude|ca-value|country-code) (\S+)'), r'\1 xxxxxx'),
-
+
# Strip SNMP location
(args.snmp, re.compile(r'(location) \S+'), r'\1 xxxxxx'),
]
strip_lines(stripping_rules)
-
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
deleted file mode 100755
index 097d28d85..000000000
--- a/src/helpers/vyos-bridge-sync.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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/>.
-#
-
-# Script is used to synchronize configured bridge interfaces.
-# one can add a non existing interface to a bridge group (e.g. VLAN)
-# but the vlan interface itself does yet not exist. It should be added
-# to the bridge automatically once it's available
-
-import argparse
-from sys import exit
-from time import sleep
-
-from vyos.config import Config
-from vyos.util import cmd, run
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True)
- args, unknownargs = parser.parse_known_args()
-
- conf = Config()
- if not conf.list_nodes('interfaces bridge'):
- # no bridge interfaces exist .. bail out early
- exit(0)
- else:
- for bridge in conf.list_nodes('interfaces bridge'):
- for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
- if args.interface == member_if:
- command = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
- # let interfaces etc. settle - especially required for OpenVPN bridged interfaces
- sleep(4)
- # XXX: This is ignoring any issue, should be cmd but kept as it
- # XXX: during the migration to not cause any regression
- run(command)
-
- exit(0)
diff --git a/src/migration-scripts/https/2-to-3 b/src/migration-scripts/https/2-to-3
new file mode 100755
index 000000000..fa29fdd18
--- /dev/null
+++ b/src/migration-scripts/https/2-to-3
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# * Migrate system signed certificate to use PKI
+
+import sys
+
+from vyos.configtree import ConfigTree
+from vyos.pki import create_certificate
+from vyos.pki import create_certificate_request
+from vyos.pki import create_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'https', 'certificates']
+pki_base = ['pki']
+
+if not config.exists(base + ['system-generated-certificate']):
+ sys.exit(0)
+
+def wrapped_pem_to_config_value(pem):
+ out = []
+ for line in pem.strip().split("\n"):
+ if not line or line.startswith("-----") or line[0] == '#':
+ continue
+ out.append(line)
+ return "".join(out)
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+valid_days = 365
+if config.exists(base + ['system-generated-certificate', 'lifetime']):
+ valid_days = int(config.return_value(base + ['system-generated-certificate', 'lifetime']))
+
+key = create_private_key('rsa', 2048)
+subject = {'country': 'GB', 'state': 'N/A', 'locality': 'N/A', 'organization': 'VyOS', 'common_name': 'vyos'}
+cert_req = create_certificate_request(subject, key, ['vyos'])
+cert = create_certificate(cert_req, cert_req, key, valid_days)
+
+if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', 'generated_https', 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+
+if key:
+ key_pem = encode_private_key(key)
+ config.set(pki_base + ['certificate', 'generated_https', 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+
+if cert and key:
+ config.set(base + ['certificate'], value='generated_https')
+else:
+ print('Failed to migrate system-generated-certificate from https service')
+
+config.delete(base + ['system-generated-certificate'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20
index 06e07572f..e96663e54 100755
--- a/src/migration-scripts/interfaces/19-to-20
+++ b/src/migration-scripts/interfaces/19-to-20
@@ -18,62 +18,6 @@ from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-def migrate_ospf(config, path, interface):
- path = path + ['ospf']
- if config.exists(path):
- new_base = ['protocols', 'ospf', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ip ospf" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_ospfv3(config, path, interface):
- path = path + ['ospfv3']
- if config.exists(path):
- new_base = ['protocols', 'ospfv3', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ipv6 ospfv3" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_rip(config, path, interface):
- path = path + ['rip']
- if config.exists(path):
- new_base = ['protocols', 'rip', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ip rip" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_ripng(config, path, interface):
- path = path + ['ripng']
- if config.exists(path):
- new_base = ['protocols', 'ripng', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ipv6 ripng" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
if __name__ == '__main__':
if (len(argv) < 1):
print("Must specify file name!")
@@ -85,57 +29,29 @@ if __name__ == '__main__':
config = ConfigTree(config_file)
- #
- # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
- #
- for type in config.list_nodes(['interfaces']):
- for interface in config.list_nodes(['interfaces', type]):
- ip_base = ['interfaces', type, interface, 'ip']
- ipv6_base = ['interfaces', type, interface, 'ipv6']
- migrate_rip(config, ip_base, interface)
- migrate_ripng(config, ipv6_base, interface)
- migrate_ospf(config, ip_base, interface)
- migrate_ospfv3(config, ipv6_base, interface)
-
- vif_path = ['interfaces', type, interface, 'vif']
- if config.exists(vif_path):
- for vif in config.list_nodes(vif_path):
- vif_ip_base = vif_path + [vif, 'ip']
- vif_ipv6_base = vif_path + [vif, 'ipv6']
- ifname = f'{interface}.{vif}'
-
- migrate_rip(config, vif_ip_base, ifname)
- migrate_ripng(config, vif_ipv6_base, ifname)
- migrate_ospf(config, vif_ip_base, ifname)
- migrate_ospfv3(config, vif_ipv6_base, ifname)
-
-
- vif_s_path = ['interfaces', type, interface, 'vif-s']
- if config.exists(vif_s_path):
- for vif_s in config.list_nodes(vif_s_path):
- vif_s_ip_base = vif_s_path + [vif_s, 'ip']
- vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
-
- # vif-c interfaces MUST be migrated before their parent vif-s
- # interface as the migrate_*() functions delete the path!
- vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
- if config.exists(vif_c_path):
- for vif_c in config.list_nodes(vif_c_path):
- vif_c_ip_base = vif_c_path + [vif_c, 'ip']
- vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
- ifname = f'{interface}.{vif_s}.{vif_c}'
-
- migrate_rip(config, vif_c_ip_base, ifname)
- migrate_ripng(config, vif_c_ipv6_base, ifname)
- migrate_ospf(config, vif_c_ip_base, ifname)
- migrate_ospfv3(config, vif_c_ipv6_base, ifname)
-
-
- ifname = f'{interface}.{vif_s}'
- migrate_rip(config, vif_s_ip_base, ifname)
- migrate_ripng(config, vif_s_ipv6_base, ifname)
- migrate_ospf(config, vif_s_ip_base, ifname)
- migrate_ospfv3(config, vif_s_ipv6_base, ifname)
+ for type in ['tunnel', 'l2tpv3']:
+ base = ['interfaces', type]
+ if not config.exists(base):
+ # Nothing to do
+ continue
+
+ for interface in config.list_nodes(base):
+ # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap
+ encap_path = base + [interface, 'encapsulation']
+ if type == 'tunnel' and config.exists(encap_path):
+ tmp = config.return_value(encap_path)
+ if tmp == 'gre-bridge':
+ config.set(encap_path, value='gretap')
+
+ # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address
+ # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote
+ local_ip_path = base + [interface, 'local-ip']
+ if config.exists(local_ip_path):
+ config.rename(local_ip_path, 'source-address')
+
+ remote_ip_path = base + [interface, 'remote-ip']
+ if config.exists(remote_ip_path):
+ config.rename(remote_ip_path, 'remote')
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21
index e96663e54..06e07572f 100755
--- a/src/migration-scripts/interfaces/20-to-21
+++ b/src/migration-scripts/interfaces/20-to-21
@@ -18,6 +18,62 @@ from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
+def migrate_ospf(config, path, interface):
+ path = path + ['ospf']
+ if config.exists(path):
+ new_base = ['protocols', 'ospf', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip ospf" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ospfv3(config, path, interface):
+ path = path + ['ospfv3']
+ if config.exists(path):
+ new_base = ['protocols', 'ospfv3', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ospfv3" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_rip(config, path, interface):
+ path = path + ['rip']
+ if config.exists(path):
+ new_base = ['protocols', 'rip', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip rip" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ripng(config, path, interface):
+ path = path + ['ripng']
+ if config.exists(path):
+ new_base = ['protocols', 'ripng', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ripng" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
if __name__ == '__main__':
if (len(argv) < 1):
print("Must specify file name!")
@@ -29,29 +85,57 @@ if __name__ == '__main__':
config = ConfigTree(config_file)
- for type in ['tunnel', 'l2tpv3']:
- base = ['interfaces', type]
- if not config.exists(base):
- # Nothing to do
- continue
-
- for interface in config.list_nodes(base):
- # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap
- encap_path = base + [interface, 'encapsulation']
- if type == 'tunnel' and config.exists(encap_path):
- tmp = config.return_value(encap_path)
- if tmp == 'gre-bridge':
- config.set(encap_path, value='gretap')
-
- # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address
- # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote
- local_ip_path = base + [interface, 'local-ip']
- if config.exists(local_ip_path):
- config.rename(local_ip_path, 'source-address')
-
- remote_ip_path = base + [interface, 'remote-ip']
- if config.exists(remote_ip_path):
- config.rename(remote_ip_path, 'remote')
+ #
+ # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
+ #
+ for type in config.list_nodes(['interfaces']):
+ for interface in config.list_nodes(['interfaces', type]):
+ ip_base = ['interfaces', type, interface, 'ip']
+ ipv6_base = ['interfaces', type, interface, 'ipv6']
+ migrate_rip(config, ip_base, interface)
+ migrate_ripng(config, ipv6_base, interface)
+ migrate_ospf(config, ip_base, interface)
+ migrate_ospfv3(config, ipv6_base, interface)
+
+ vif_path = ['interfaces', type, interface, 'vif']
+ if config.exists(vif_path):
+ for vif in config.list_nodes(vif_path):
+ vif_ip_base = vif_path + [vif, 'ip']
+ vif_ipv6_base = vif_path + [vif, 'ipv6']
+ ifname = f'{interface}.{vif}'
+
+ migrate_rip(config, vif_ip_base, ifname)
+ migrate_ripng(config, vif_ipv6_base, ifname)
+ migrate_ospf(config, vif_ip_base, ifname)
+ migrate_ospfv3(config, vif_ipv6_base, ifname)
+
+
+ vif_s_path = ['interfaces', type, interface, 'vif-s']
+ if config.exists(vif_s_path):
+ for vif_s in config.list_nodes(vif_s_path):
+ vif_s_ip_base = vif_s_path + [vif_s, 'ip']
+ vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
+
+ # vif-c interfaces MUST be migrated before their parent vif-s
+ # interface as the migrate_*() functions delete the path!
+ vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
+ if config.exists(vif_c_path):
+ for vif_c in config.list_nodes(vif_c_path):
+ vif_c_ip_base = vif_c_path + [vif_c, 'ip']
+ vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
+ ifname = f'{interface}.{vif_s}.{vif_c}'
+
+ migrate_rip(config, vif_c_ip_base, ifname)
+ migrate_ripng(config, vif_c_ipv6_base, ifname)
+ migrate_ospf(config, vif_c_ip_base, ifname)
+ migrate_ospfv3(config, vif_c_ipv6_base, ifname)
+
+
+ ifname = f'{interface}.{vif_s}'
+ migrate_rip(config, vif_s_ip_base, ifname)
+ migrate_ripng(config, vif_s_ipv6_base, ifname)
+ migrate_ospf(config, vif_s_ip_base, ifname)
+ migrate_ospfv3(config, vif_s_ipv6_base, ifname)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23
new file mode 100755
index 000000000..93ce9215f
--- /dev/null
+++ b/src/migration-scripts/interfaces/22-to-23
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# Migrate Wireguard to store keys in CLI
+# Migrate EAPoL to PKI configuration
+
+import os
+import sys
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_dh_parameters
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+def wrapped_pem_to_config_value(pem):
+ out = []
+ for line in pem.strip().split("\n"):
+ if not line or line.startswith("-----") or line[0] == '#':
+ continue
+ out.append(line)
+ return "".join(out)
+
+def read_file_for_pki(config_auth_path):
+ full_path = os.path.join(AUTH_DIR, config_auth_path)
+ output = None
+
+ if os.path.isfile(full_path):
+ if not os.access(full_path, os.R_OK):
+ run(f'sudo chmod 644 {full_path}')
+
+ with open(full_path, 'r') as f:
+ output = f.read()
+
+ return output
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+AUTH_DIR = '/config/auth'
+pki_base = ['pki']
+
+# OpenVPN
+base = ['interfaces', 'openvpn']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ x509_base = base + [interface, 'tls']
+ pki_name = f'openvpn_{interface}'
+
+ if config.exists(base + [interface, 'shared-secret-key-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'shared-secret-key-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_shared'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'shared-secret-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate shared-secret-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'shared-secret-key-file'])
+
+ if not config.exists(base + [interface, 'tls']):
+ continue
+
+ if config.exists(base + [interface, 'tls', 'auth-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'auth-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_auth'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate auth-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'auth-file'])
+
+ if config.exists(base + [interface, 'tls', 'crypt-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'crypt-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_crypt'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate crypt-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'crypt-file'])
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['crl-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ crl_file = config.return_value(x509_base + ['crl-file'])
+ crl_path = os.path.join(AUTH_DIR, crl_file)
+ crl = None
+
+ if os.path.isfile(crl_path):
+ if not os.access(crl_path, os.R_OK):
+ run(f'sudo chmod 644 {crl_path}')
+
+ with open(crl_path, 'r') as f:
+ crl_data = f.read()
+ crl = load_crl(crl_data, wrap_tags=False)
+
+ if crl:
+ crl_pem = encode_certificate(crl)
+ config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
+ else:
+ print(f'Failed to migrate CRL on openvpn interface {interface}')
+
+ config.delete(x509_base + ['crl-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on openvpn interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+ if config.exists(x509_base + ['dh-file']):
+ if not config.exists(pki_base + ['dh']):
+ config.set(pki_base + ['dh'])
+ config.set_tag(pki_base + ['dh'])
+
+ dh_file = config.return_value(x509_base + ['dh-file'])
+ dh_path = os.path.join(AUTH_DIR, dh_file)
+ dh = None
+
+ if os.path.isfile(dh_path):
+ if not os.access(dh_path, os.R_OK):
+ run(f'sudo chmod 644 {dh_path}')
+
+ with open(dh_path, 'r') as f:
+ dh_data = f.read()
+ dh = load_dh_parameters(dh_data, wrap_tags=False)
+
+ if dh:
+ dh_pem = encode_dh_parameters(dh)
+ config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem))
+ config.set(x509_base + ['dh-params'], value=pki_name)
+ else:
+ print(f'Failed to migrate DH parameters on openvpn interface {interface}')
+
+ config.delete(x509_base + ['dh-file'])
+
+# Wireguard
+base = ['interfaces', 'wireguard']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ private_key_path = base + [interface, 'private-key']
+
+ key_file = 'default'
+ if config.exists(private_key_path):
+ key_file = config.return_value(private_key_path)
+
+ full_key_path = f'/config/auth/wireguard/{key_file}/private.key'
+
+ if not os.path.exists(full_key_path):
+ print(f'Could not find wireguard private key for migration on interface "{interface}"')
+ continue
+
+ with open(full_key_path, 'r') as f:
+ key_data = f.read().strip()
+ config.set(private_key_path, value=key_data)
+
+ for peer in config.list_nodes(base + [interface, 'peer']):
+ config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key')
+
+# Ethernet EAPoL
+base = ['interfaces', 'ethernet']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ if not config.exists(base + [interface, 'eapol']):
+ continue
+
+ x509_base = base + [interface, 'eapol']
+ pki_name = f'eapol_{interface}'
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6
index ba5ce0fca..e9adee01b 100755
--- a/src/migration-scripts/ipsec/5-to-6
+++ b/src/migration-scripts/ipsec/5-to-6
@@ -74,6 +74,17 @@ log_mode = log + ['log-modes']
if config.exists(log_mode):
config.rename(log_mode, 'subsystem')
+# Rename "ipsec-interfaces interface" to "interface"
+base_interfaces = base + ['ipsec-interfaces', 'interface']
+if config.exists(base_interfaces):
+ config.copy(base_interfaces, base + ['interface'])
+ config.delete(base_interfaces)
+
+# Remove deprecated "auto-update" option
+tmp = base + ['auto-update']
+if config.exists(tmp):
+ config.delete(tmp)
+
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
diff --git a/src/migration-scripts/l2tp/3-to-4 b/src/migration-scripts/l2tp/3-to-4
new file mode 100755
index 000000000..18eabadec
--- /dev/null
+++ b/src/migration-scripts/l2tp/3-to-4
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# - remove primary/secondary identifier from nameserver
+# - TODO: remove radius server req-limit
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['authentication', 'x509']):
+ exit(0)
+
+x509_base = base + ['authentication', 'x509']
+pki_name = 'l2tp_remote_access'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on l2tp remote-access config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['crl-file']):
+ crl_file = config.return_value(x509_base + ['crl-file'])
+ crl_path = os.path.join(AUTH_DIR, crl_file)
+ crl = None
+
+ if os.path.isfile(crl_path):
+ if not os.access(crl_path, os.R_OK):
+ run(f'sudo chmod 644 {crl_path}')
+
+ with open(crl_path, 'r') as f:
+ crl_data = f.read()
+ crl = load_certificate(crl_data, wrap_tags=False)
+
+ if crl:
+ crl_pem = encode_certificate(crl)
+ config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
+ else:
+ print(f'Failed to migrate CRL on l2tp remote-access config')
+
+ config.delete(x509_base + ['crl-file'])
+
+if config.exists(x509_base + ['server-cert-file']):
+ cert_file = config.return_value(x509_base + ['server-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on l2tp remote-access config')
+
+ config.delete(x509_base + ['server-cert-file'])
+
+if config.exists(x509_base + ['server-key-file']):
+ key_file = config.return_value(x509_base + ['server-key-file'])
+ key_passphrase = None
+
+ if config.exists(x509_base + ['server-key-password']):
+ key_passphrase = config.return_value(x509_base + ['server-key-password'])
+
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=key_passphrase)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+
+ if key_passphrase:
+ config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected'])
+ config.set(x509_base + ['private-key-passphrase'], value=key_passphrase)
+ else:
+ print(f'Failed to migrate private key on l2tp remote-access config')
+
+ config.delete(x509_base + ['server-key-file'])
+ if config.exists(x509_base + ['server-key-password']):
+ config.delete(x509_base + ['server-key-password'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/openconnect/0-to-1 b/src/migration-scripts/openconnect/0-to-1
new file mode 100755
index 000000000..83cd09143
--- /dev/null
+++ b/src/migration-scripts/openconnect/0-to-1
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# - Update SSL to use PKI configuration
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'openconnect']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['ssl']):
+ exit(0)
+
+x509_base = base + ['ssl']
+pki_name = 'openconnect'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on openconnect config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['cert-file']):
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on openconnect config')
+
+ config.delete(x509_base + ['cert-file'])
+
+if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on openconnect config')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/quagga/7-to-8 b/src/migration-scripts/quagga/7-to-8
index 9c277a6f1..15c44924f 100755
--- a/src/migration-scripts/quagga/7-to-8
+++ b/src/migration-scripts/quagga/7-to-8
@@ -14,61 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# - T2450: drop interface-route and interface-route6 from "protocols static"
+# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths"
+# under the IPv4 address-family tree. Reason is we currently have no way in
+# configuring this for IPv6 address-family. This mimics the FRR configuration.
from sys import argv
from sys import exit
-
from vyos.configtree import ConfigTree
-def migrate_interface_route(config, base, path, route_route6):
- """ Generic migration function which can be called on every instance of
- interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes.
-
- What we do?
- - Drop 'interface-route' or 'interface-route6' and migrate the route unter the
- 'route' or 'route6' tag node.
- """
- if config.exists(base + path):
- for route in config.list_nodes(base + path):
- interface = config.list_nodes(base + path + [route, 'next-hop-interface'])
-
- tmp = base + path + [route, 'next-hop-interface']
- for interface in config.list_nodes(tmp):
- new_base = base + [route_route6, route, 'interface']
- config.set(new_base)
- config.set_tag(base + [route_route6])
- config.set_tag(new_base)
- config.copy(tmp + [interface], new_base + [interface])
-
- config.delete(base + path)
-
-def migrate_route(config, base, path, route_route6):
- """ Generic migration function which can be called on every instance of
- route, beeing it ipv4, ipv6 or even nested under the static table nodes.
-
- What we do?
- - for consistency reasons rename next-hop-interface to interface
- - for consistency reasons rename next-hop-vrf to vrf
- """
- if config.exists(base + path):
- for route in config.list_nodes(base + path):
- next_hop = base + path + [route, 'next-hop']
- if config.exists(next_hop):
- for gateway in config.list_nodes(next_hop):
- # IPv4 routes calls it next-hop-interface, rename this to
- # interface instead so it's consitent with IPv6
- interface_path = next_hop + [gateway, 'next-hop-interface']
- if config.exists(interface_path):
- config.rename(interface_path, 'interface')
-
- # When VRFs got introduced, I (c-po) named it next-hop-vrf,
- # we can also call it vrf which is simply shorter.
- vrf_path = next_hop + [gateway, 'next-hop-vrf']
- if config.exists(vrf_path):
- config.rename(vrf_path, 'vrf')
-
-
if (len(argv) < 2):
print("Must specify file name!")
exit(1)
@@ -78,41 +31,27 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['protocols', 'static']
-
+base = ['protocols', 'bgp']
config = ConfigTree(config_file)
+
if not config.exists(base):
# Nothing to do
exit(0)
-# Migrate interface-route into route
-migrate_interface_route(config, base, ['interface-route'], 'route')
-
-# Migrate interface-route6 into route6
-migrate_interface_route(config, base, ['interface-route6'], 'route6')
-
-# Cleanup nodes inside route
-migrate_route(config, base, ['route'], 'route')
-
-# Cleanup nodes inside route6
-migrate_route(config, base, ['route6'], 'route6')
-
-#
-# PBR table cleanup
-table_path = base + ['table']
-if config.exists(table_path):
- for table in config.list_nodes(table_path):
- # Migrate interface-route into route
- migrate_interface_route(config, table_path + [table], ['interface-route'], 'route')
-
- # Migrate interface-route6 into route6
- migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6')
-
- # Cleanup nodes inside route
- migrate_route(config, table_path + [table], ['route'], 'route')
-
- # Cleanup nodes inside route6
- migrate_route(config, table_path + [table], ['route6'], 'route6')
+# Check if BGP is actually configured and obtain the ASN
+asn_list = config.list_nodes(base)
+if asn_list:
+ # There's always just one BGP node, if any
+ bgp_base = base + [asn_list[0]]
+
+ maximum_paths = bgp_base + ['maximum-paths']
+ if config.exists(maximum_paths):
+ for bgp_type in ['ebgp', 'ibgp']:
+ if config.exists(maximum_paths + [bgp_type]):
+ new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths']
+ config.set(new_base)
+ config.copy(maximum_paths + [bgp_type], new_base + [bgp_type])
+ config.delete(maximum_paths)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9
index 15c44924f..38507bd3d 100755
--- a/src/migration-scripts/quagga/8-to-9
+++ b/src/migration-scripts/quagga/8-to-9
@@ -14,14 +14,76 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths"
-# under the IPv4 address-family tree. Reason is we currently have no way in
-# configuring this for IPv6 address-family. This mimics the FRR configuration.
+# - T2450: drop interface-route and interface-route6 from "protocols static"
from sys import argv
from sys import exit
+
from vyos.configtree import ConfigTree
+def migrate_interface_route(config, base, path, route_route6):
+ """ Generic migration function which can be called on every instance of
+ interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes.
+
+ What we do?
+ - Drop 'interface-route' or 'interface-route6' and migrate the route unter the
+ 'route' or 'route6' tag node.
+ """
+ if config.exists(base + path):
+ for route in config.list_nodes(base + path):
+ interface = config.list_nodes(base + path + [route, 'next-hop-interface'])
+
+ tmp = base + path + [route, 'next-hop-interface']
+ for interface in config.list_nodes(tmp):
+ new_base = base + [route_route6, route, 'interface']
+ config.set(new_base)
+ config.set_tag(base + [route_route6])
+ config.set_tag(new_base)
+ config.copy(tmp + [interface], new_base + [interface])
+
+ config.delete(base + path)
+
+def migrate_route(config, base, path, route_route6):
+ """ Generic migration function which can be called on every instance of
+ route, beeing it ipv4, ipv6 or even nested under the static table nodes.
+
+ What we do?
+ - for consistency reasons rename next-hop-interface to interface
+ - for consistency reasons rename next-hop-vrf to vrf
+ """
+ if config.exists(base + path):
+ for route in config.list_nodes(base + path):
+ next_hop = base + path + [route, 'next-hop']
+ if config.exists(next_hop):
+ for gateway in config.list_nodes(next_hop):
+ # IPv4 routes calls it next-hop-interface, rename this to
+ # interface instead so it's consitent with IPv6
+ interface_path = next_hop + [gateway, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+
+ # When VRFs got introduced, I (c-po) named it next-hop-vrf,
+ # we can also call it vrf which is simply shorter.
+ vrf_path = next_hop + [gateway, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
+ next_hop = base + path + [route, 'interface']
+ if config.exists(next_hop):
+ for interface in config.list_nodes(next_hop):
+ # IPv4 routes calls it next-hop-interface, rename this to
+ # interface instead so it's consitent with IPv6
+ interface_path = next_hop + [interface, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+
+ # When VRFs got introduced, I (c-po) named it next-hop-vrf,
+ # we can also call it vrf which is simply shorter.
+ vrf_path = next_hop + [interface, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
+
if (len(argv) < 2):
print("Must specify file name!")
exit(1)
@@ -31,27 +93,41 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['protocols', 'bgp']
-config = ConfigTree(config_file)
+base = ['protocols', 'static']
+config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
exit(0)
-# Check if BGP is actually configured and obtain the ASN
-asn_list = config.list_nodes(base)
-if asn_list:
- # There's always just one BGP node, if any
- bgp_base = base + [asn_list[0]]
-
- maximum_paths = bgp_base + ['maximum-paths']
- if config.exists(maximum_paths):
- for bgp_type in ['ebgp', 'ibgp']:
- if config.exists(maximum_paths + [bgp_type]):
- new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths']
- config.set(new_base)
- config.copy(maximum_paths + [bgp_type], new_base + [bgp_type])
- config.delete(maximum_paths)
+# Migrate interface-route into route
+migrate_interface_route(config, base, ['interface-route'], 'route')
+
+# Migrate interface-route6 into route6
+migrate_interface_route(config, base, ['interface-route6'], 'route6')
+
+# Cleanup nodes inside route
+migrate_route(config, base, ['route'], 'route')
+
+# Cleanup nodes inside route6
+migrate_route(config, base, ['route6'], 'route6')
+
+#
+# PBR table cleanup
+table_path = base + ['table']
+if config.exists(table_path):
+ for table in config.list_nodes(table_path):
+ # Migrate interface-route into route
+ migrate_interface_route(config, table_path + [table], ['interface-route'], 'route')
+
+ # Migrate interface-route6 into route6
+ migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6')
+
+ # Cleanup nodes inside route
+ migrate_route(config, table_path + [table], ['route'], 'route')
+
+ # Cleanup nodes inside route6
+ migrate_route(config, table_path + [table], ['route6'], 'route6')
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/sstp/3-to-4 b/src/migration-scripts/sstp/3-to-4
new file mode 100755
index 000000000..0568f043f
--- /dev/null
+++ b/src/migration-scripts/sstp/3-to-4
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# - Update SSL to use PKI configuration
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'sstp']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['ssl']):
+ exit(0)
+
+x509_base = base + ['ssl']
+pki_name = 'sstp'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on sstp config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['cert-file']):
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on sstp config')
+
+ config.delete(x509_base + ['cert-file'])
+
+if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on sstp config')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/vrf/0-to-1 b/src/migration-scripts/vrf/0-to-1
index 29b2fab74..2b41ef3c7 100755
--- a/src/migration-scripts/vrf/0-to-1
+++ b/src/migration-scripts/vrf/0-to-1
@@ -91,6 +91,16 @@ for vrf in config.list_nodes(base):
if config.exists(vrf_path):
config.rename(vrf_path, 'vrf')
+ next_hop = route_path + [route, 'interface']
+ if config.exists(next_hop):
+ for interface in config.list_nodes(next_hop):
+ interface_path = next_hop + [interface, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+ vrf_path = next_hop + [interface, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
#
# Cleanup nodes inside route6
#
diff --git a/src/migration-scripts/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2
index 20128e957..9bc704e02 100755
--- a/src/migration-scripts/vrf/1-to-2
+++ b/src/migration-scripts/vrf/1-to-2
@@ -49,6 +49,7 @@ for vrf in config.list_nodes(base):
new_static_base = vrf_base + [vrf, 'protocols']
config.set(new_static_base)
config.copy(static_base, new_static_base + ['static'])
+ config.set_tag(new_static_base + ['static', 'route'])
# Now delete the old configuration
config.delete(base)
diff --git a/src/migration-scripts/vrf/2-to-3 b/src/migration-scripts/vrf/2-to-3
new file mode 100755
index 000000000..8e0f97141
--- /dev/null
+++ b/src/migration-scripts/vrf/2-to-3
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# Since connection tracking zones are int16, VRFs tables maximum value must
+# be limited to 65535
+# Also, interface names in nftables cannot start from numbers,
+# so VRF name should not start from a number
+
+from sys import argv
+from sys import exit
+from random import randrange
+from random import choice
+from string import ascii_lowercase
+from vyos.configtree import ConfigTree
+import re
+
+
+# Helper function to find all config items with a VRF name
+def _search_vrfs(config_commands, vrf_name):
+ vrf_values = []
+ # Regex to find path of config command with old VRF
+ regex_filter = re.compile(rf'^set (?P<cmd_path>[^\']+vrf) \'{vrf_name}\'$')
+ # Check each command for VRF value
+ for config_command in config_commands:
+ search_result = regex_filter.search(config_command)
+ if search_result:
+ # Append VRF command to a list
+ vrf_values.append(search_result.group('cmd_path').split())
+ if vrf_values:
+ return vrf_values
+ else:
+ return None
+
+
+# Helper function to find all config items with a table number
+def _search_tables(config_commands, table_num):
+ table_items = {'table_tags': [], 'table_values': []}
+ # Regex to find values and nodes with a table number
+ regex_tags = re.compile(rf'^set (?P<cmd_path>[^\']+table {table_num}) ?.*$')
+ regex_values = re.compile(
+ rf'^set (?P<cmd_path>[^\']+table) \'{table_num}\'$')
+ for config_command in config_commands:
+ # Search for tag nodes
+ search_result = regex_tags.search(config_command)
+ if search_result:
+ # Append table node path to a tag nodes list
+ cmd_path = search_result.group('cmd_path').split()
+ if cmd_path not in table_items['table_tags']:
+ table_items['table_tags'].append(cmd_path)
+ # Search for value nodes
+ search_result = regex_values.search(config_command)
+ if search_result:
+ # Append table node path to a value nodes list
+ table_items['table_values'].append(
+ search_result.group('cmd_path').split())
+ return table_items
+
+
+if (len(argv) < 2):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['vrf', 'name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# Get a list of all currently used VRFs and tables
+vrfs_current = {}
+for vrf in config.list_nodes(base):
+ vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table']))
+
+# Check VRF names and table numbers
+name_regex = re.compile(r'^\d.*$')
+for vrf_name, vrf_table in vrfs_current.items():
+ # Check table number
+ if vrf_table > 65535:
+ # Find new unused table number
+ vrfs_current[vrf_name] = None
+ while not vrfs_current[vrf_name]:
+ table_random = randrange(100, 65535)
+ if table_random not in vrfs_current.values():
+ vrfs_current[vrf_name] = table_random
+ # Update number to a new one
+ config.set(['vrf', 'name', vrf_name, 'table'],
+ vrfs_current[vrf_name],
+ replace=True)
+ # Check config items with old table number and replace to new one
+ config_commands = config.to_commands().split('\n')
+ table_config_lines = _search_tables(config_commands, vrf_table)
+ # Rename table nodes
+ if table_config_lines.get('table_tags'):
+ for table_config_path in table_config_lines.get('table_tags'):
+ config.rename(table_config_path, f'{vrfs_current[vrf_name]}')
+ # Replace table values
+ if table_config_lines.get('table_values'):
+ for table_config_path in table_config_lines.get('table_values'):
+ config.set(table_config_path,
+ f'{vrfs_current[vrf_name]}',
+ replace=True)
+
+ # Check VRF name
+ if name_regex.match(vrf_name):
+ vrf_name_new = None
+ while not vrf_name_new:
+ vrf_name_rand = f'{choice(ascii_lowercase)}{vrf_name}'[:15]
+ if vrf_name_rand not in vrfs_current:
+ vrf_name_new = vrf_name_rand
+ # Update VRF name to a new one
+ config.rename(['vrf', 'name', vrf_name], vrf_name_new)
+ # Check config items with old VRF name and replace to new one
+ config_commands = config.to_commands().split('\n')
+ vrf_config_lines = _search_vrfs(config_commands, vrf_name)
+ # Rename VRF to a new name
+ if vrf_config_lines:
+ for vrf_value_path in vrf_config_lines:
+ config.set(vrf_value_path, vrf_name_new, replace=True)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
index 1fb61d263..d79b6c024 100755
--- a/src/op_mode/dns_forwarding_statistics.py
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -11,7 +11,7 @@ PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
OUT_TMPL_SRC = """
DNS forwarding statistics:
-Cache entries: {{ cache_entries -}}
+Cache entries: {{ cache_entries }}
Cache size: {{ cache_size }} kbytes
"""
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
new file mode 100755
index 000000000..d45525431
--- /dev/null
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+import argparse
+
+from jinja2 import Template
+from sys import exit
+from socket import getfqdn
+from cryptography.x509.oid import NameOID
+
+from vyos.config import Config
+from vyos.pki import load_certificate
+from vyos.template import render_to_string
+from vyos.util import ask_input
+
+# Apple profiles only support one IKE/ESP encryption cipher and hash, whereas
+# VyOS comes with a multitude of different proposals for a connection.
+#
+# We take all available proposals from the VyOS CLI and ask the user which one
+# he would like to get enabled in his profile - thus there is limited possibility
+# to select a proposal that is not supported on the connection profile.
+#
+# IOS supports IKE-SA encryption algorithms:
+# - DES
+# - 3DES
+# - AES-128
+# - AES-256
+# - AES-128-GCM
+# - AES-256-GCM
+# - ChaCha20Poly1305
+#
+vyos2apple_cipher = {
+ '3des' : '3DES',
+ 'aes128' : 'AES-128',
+ 'aes256' : 'AES-256',
+ 'aes128gcm128' : 'AES-128-GCM',
+ 'aes256gcm128' : 'AES-256-GCM',
+ 'chacha20poly1305' : 'ChaCha20Poly1305',
+}
+
+# Windows supports IKE-SA encryption algorithms:
+# - DES3
+# - AES128
+# - AES192
+# - AES256
+# - GCMAES128
+# - GCMAES192
+# - GCMAES256
+#
+vyos2windows_cipher = {
+ '3des' : 'DES3',
+ 'aes128' : 'AES128',
+ 'aes192' : 'AES192',
+ 'aes256' : 'AES256',
+ 'aes128gcm128' : 'GCMAES128',
+ 'aes192gcm128' : 'GCMAES192',
+ 'aes256gcm128' : 'GCMAES256',
+}
+
+# IOS supports IKE-SA integrity algorithms:
+# - SHA1-96
+# - SHA1-160
+# - SHA2-256
+# - SHA2-384
+# - SHA2-512
+#
+vyos2apple_integrity = {
+ 'sha1' : 'SHA1-96',
+ 'sha1_160' : 'SHA1-160',
+ 'sha256' : 'SHA2-256',
+ 'sha384' : 'SHA2-384',
+ 'sha512' : 'SHA2-512',
+}
+
+# Windows supports IKE-SA integrity algorithms:
+# - SHA1-96
+# - SHA1-160
+# - SHA2-256
+# - SHA2-384
+# - SHA2-512
+#
+vyos2windows_integrity = {
+ 'sha1' : 'SHA196',
+ 'sha256' : 'SHA256',
+ 'aes128gmac' : 'GCMAES128',
+ 'aes192gmac' : 'GCMAES192',
+ 'aes256gmac' : 'GCMAES256',
+}
+
+# IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would
+# be: 14, 15, 16, 17, 18, 19, 20, 21, 31
+ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
+# Windows 10 only allows a limited set of DH groups
+windows_supported_dh_groups = ['1', '2', '14', '24']
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
+parser.add_argument("--connection", action="store", help='IPsec IKEv2 remote-access connection name from CLI', required=True)
+parser.add_argument("--remote", action="store", help='VPN connection remote-address where the client will connect to', required=True)
+parser.add_argument("--profile", action="store", help='IKEv2 profile name used in the profile list on the device')
+parser.add_argument("--name", action="store", help='VPN connection name as seen in the VPN application later')
+args = parser.parse_args()
+
+ipsec_base = ['vpn', 'ipsec']
+config_base = ipsec_base + ['remote-access', 'connection']
+pki_base = ['pki']
+conf = Config()
+if not conf.exists(config_base):
+ exit('IPSec remote-access is not configured!')
+
+profile_name = 'VyOS IKEv2 Profile'
+if args.profile:
+ profile_name = args.profile
+
+vpn_name = 'VyOS IKEv2 VPN'
+if args.name:
+ vpn_name = args.name
+
+conn_base = config_base + [args.connection]
+if not conf.exists(conn_base):
+ exit(f'IPSec remote-access connection "{args.connection}" does not exist!')
+
+data = conf.get_config_dict(conn_base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+data['profile_name'] = profile_name
+data['vpn_name'] = vpn_name
+data['remote'] = args.remote
+# This is a reverse-DNS style unique identifier used to detect duplicate profiles
+tmp = getfqdn().split('.')
+tmp = reversed(tmp)
+data['rfqdn'] = '.'.join(tmp)
+
+pki = conf.get_config_dict(pki_base, get_first_key=True)
+ca_name = data['authentication']['x509']['ca_certificate']
+cert_name = data['authentication']['x509']['certificate']
+
+ca_cert = load_certificate(pki['ca'][ca_name]['certificate'])
+cert = load_certificate(pki['certificate'][cert_name]['certificate'])
+
+data['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['ca_cert'] = conf.return_value(pki_base + ['ca', ca_name, 'certificate'])
+
+esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
+ key_mangling=('-', '_'), get_first_key=True)
+ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'],
+ key_mangling=('-', '_'), get_first_key=True)
+
+
+# This script works only for Apple iOS/iPadOS and Windows. Both operating systems
+# have different limitations thus we load the limitations based on the operating
+# system used.
+
+vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher;
+vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity;
+supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups;
+
+# Create a dictionary containing client conform IKE settings
+ike = {}
+count = 1
+for _, proposal in ike_proposal.items():
+ if {'dh_group', 'encryption', 'hash'} <= set(proposal):
+ if (proposal['encryption'] in set(vyos2client_cipher) and
+ proposal['hash'] in set(vyos2client_integrity) and
+ proposal['dh_group'] in set(supported_dh_groups)):
+
+ # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+ proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+
+ ike.update( { str(count) : proposal } )
+ count += 1
+
+# Create a dictionary containing Apple conform ESP settings
+esp = {}
+count = 1
+for _, proposal in esp_proposals.items():
+ if {'encryption', 'hash'} <= set(proposal):
+ if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
+ # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+ proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+
+ esp.update( { str(count) : proposal } )
+ count += 1
+try:
+ if len(ike) > 1:
+ # Propare the input questions for the user
+ tmp = '\n'
+ for number, options in ike.items():
+ tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
+ tmp += '\nSelect one of the above IKE groups: '
+ data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
+ else:
+ data['ike_encryption'] = ike['1']
+
+ if len(esp) > 1:
+ tmp = '\n'
+ for number, options in esp.items():
+ tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
+ tmp += '\nSelect one of the above ESP groups: '
+ data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
+ else:
+ data['esp_encryption'] = esp['1']
+
+except KeyboardInterrupt:
+ exit("Interrupted")
+
+print('\n\n==== <snip> ====')
+if args.os == 'ios':
+ print(render_to_string('ipsec/ios_profile.tmpl', data))
+ print('==== </snip> ====\n')
+ print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.')
+elif args.os == 'windows':
+ print(render_to_string('ipsec/windows_profile.tmpl', data))
+ print('==== </snip> ====\n')
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 924a889db..2144ab53c 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -51,7 +51,7 @@ options = {
'help': 'Number of seconds before ping exits'
},
'do-not-fragment': {
- 'ping': '{command} -M dont',
+ 'ping': '{command} -M do',
'type': 'noarg',
'help': 'Set DF-bit flag to 1 for no fragmentation'
},
@@ -220,6 +220,8 @@ if __name__ == '__main__':
try:
ip = socket.gethostbyname(host)
+ except UnicodeError:
+ sys.exit(f'ping: Unknown host: {host}')
except socket.gaierror:
ip = host
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 7dbeb4097..297270cf1 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -38,6 +38,8 @@ from vyos.util import cmd
CERT_REQ_END = '-----END CERTIFICATE REQUEST-----'
+auth_dir = '/config/auth'
+
# Helper Functions
def get_default_values():
@@ -215,7 +217,7 @@ def install_wireguard_key(name, private_key, public_key):
print("")
print("Public key for use on peer configuration: " + public_key)
else:
- print("set interfaces wireguard [INTERFACE] peer %s pubkey '%s'" % (name, public_key))
+ print("set interfaces wireguard [INTERFACE] peer %s public-key '%s'" % (name, public_key))
print("")
print("Private key for use on peer configuration: " + private_key)
@@ -230,6 +232,22 @@ def ask_passphrase():
passphrase = ask_input('Enter passphrase:')
return passphrase
+def write_file(filename, contents):
+ full_path = os.path.join(auth_dir, filename)
+ directory = os.path.dirname(full_path)
+
+ if not os.path.exists(directory):
+ print('Failed to write file: directory does not exist')
+ return False
+
+ if os.path.exists(full_path) and not ask_yes_no('Do you want to overwrite the existing file?'):
+ return False
+
+ with open(full_path, 'w') as f:
+ f.write(contents)
+
+ print(f'File written to {full_path}')
+
# Generation functions
def generate_private_key():
@@ -266,7 +284,7 @@ def parse_san_string(san_string):
output.append(value)
return output
-def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, ask_san=True):
+def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, file=False, ask_san=True):
if not private_key:
private_key, key_type = generate_private_key()
@@ -291,14 +309,19 @@ def generate_certificate_request(private_key=None, key_type=None, return_request
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert_req))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- print("Certificate request:")
- print(encode_certificate(cert_req) + "\n")
- install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ print("Certificate request:")
+ print(encode_certificate(cert_req) + "\n")
+ install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.csr', encode_certificate(cert_req))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False):
valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True)
@@ -307,20 +330,25 @@ def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_
cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server'])
return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca)
-def generate_ca_certificate(name, install=False):
+def generate_ca_certificate(name, install=False, file=False):
private_key, key_type = generate_private_key()
cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False)
cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_ca_certificate_sign(name, ca_name, install=False):
+def generate_ca_certificate_sign(name, ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -374,14 +402,19 @@ def generate_ca_certificate_sign(name, ca_name, install=False):
cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_sign(name, ca_name, install=False):
+def generate_certificate_sign(name, ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -435,27 +468,37 @@ def generate_certificate_sign(name, ca_name, install=False):
cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_selfsign(name, install=False):
+def generate_certificate_selfsign(name, install=False, file=False):
private_key, key_type = generate_private_key()
cert_req = generate_certificate_request(private_key, key_type, return_request=True)
cert = generate_certificate(cert_req, cert_req, private_key, is_ca=False)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_revocation_list(ca_name, install=False):
+def generate_certificate_revocation_list(ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -505,26 +548,35 @@ def generate_certificate_revocation_list(ca_name, install=False):
print("Failed to create CRL")
return None
- if not install:
+ if not install and not file:
print(encode_certificate(crl))
return None
- install_crl(ca_name, crl)
+ if install:
+ install_crl(ca_name, crl)
+
+ if file:
+ write_file(f'{name}.crl', encode_certificate(crl))
-def generate_ssh_keypair(name, install=False):
+def generate_ssh_keypair(name, install=False, file=False):
private_key, key_type = generate_private_key()
public_key = private_key.public_key()
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'))
print("")
print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
return None
- install_ssh_key(name, public_key, private_key, passphrase)
+ if install:
+ install_ssh_key(name, public_key, private_key, passphrase)
+
+ if file:
+ write_file(f'{name}.pem', encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'))
+ write_file(f'{name}.key', encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
-def generate_dh_parameters(name, install=False):
+def generate_dh_parameters(name, install=False, file=False):
bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True)
print("Generating parameters...")
@@ -534,49 +586,62 @@ def generate_dh_parameters(name, install=False):
print("Failed to create DH parameters")
return None
- if not install:
+ if not install and not file:
print("DH Parameters:")
print(encode_dh_parameters(dh_params))
- install_dh_parameters(name, dh_params)
+ if install:
+ install_dh_parameters(name, dh_params)
+
+ if file:
+ write_file(f'{name}.pem', encode_dh_parameters(dh_params))
-def generate_keypair(name, install=False):
+def generate_keypair(name, install=False, file=False):
private_key, key_type = generate_private_key()
public_key = private_key.public_key()
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_public_key(public_key))
print("")
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_keypair(name, key_type, private_key, public_key, passphrase)
+ if install:
+ install_keypair(name, key_type, private_key, public_key, passphrase)
+
+ if file:
+ write_file(f'{name}.pem', encode_public_key(public_key))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_openvpn_key(name, install=False):
+def generate_openvpn_key(name, install=False, file=False):
result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"')
if not result:
print("Failed to generate OpenVPN key")
return None
- if not install:
+ if not install and not file:
print(result)
return None
- key_lines = result.split("\n")
- key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
- key_version = '1'
+ if install:
+ key_lines = result.split("\n")
+ key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
+ key_version = '1'
+
+ version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
+ if version_search:
+ key_version = version_search[1]
- version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
- if version_search:
- key_version = version_search[1]
+ print("Configure mode commands to install OpenVPN key:")
+ print("set pki openvpn shared-secret %s key '%s'" % (name, key_data))
+ print("set pki openvpn shared-secret %s version '%s'" % (name, key_version))
- print("Configure mode commands to install OpenVPN key:")
- print("set pki openvpn shared-secret %s key '%s'" % (name, key_data))
- print("set pki openvpn shared-secret %s version '%s'" % (name, key_version))
+ if file:
+ write_file(f'{name}.key', result)
-def generate_wireguard_key(name, install=False):
+def generate_wireguard_key(name, install=False, file=False):
private_key = cmd('wg genkey')
public_key = cmd('wg pubkey', input=private_key)
@@ -585,17 +650,26 @@ def generate_wireguard_key(name, install=False):
print("Public key: " + public_key)
return None
- install_wireguard_key(name, private_key, public_key)
+ if install:
+ install_wireguard_key(name, private_key, public_key)
-def generate_wireguard_psk(name, install=False):
+ if file:
+ write_file(f'{name}_public.key', public_key)
+ write_file(f'{name}_private.key', private_key)
+
+def generate_wireguard_psk(name, install=False, file=False):
psk = cmd('wg genpsk')
- if not install:
+ if not install and not file:
print("Pre-shared key:")
print(psk)
return None
- install_wireguard_psk(name, psk)
+ if install:
+ install_wireguard_psk(name, psk)
+
+ if file:
+ write_file(f'{name}.key', psk)
# Show functions
@@ -721,6 +795,7 @@ if __name__ == '__main__':
parser.add_argument('--psk', help='Wireguard pre shared key', required=False)
# Global
+ parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true')
parser.add_argument('--install', help='Install generated keys into running-config', action='store_true')
args = parser.parse_args()
@@ -729,31 +804,31 @@ if __name__ == '__main__':
if args.action == 'generate':
if args.ca:
if args.sign:
- generate_ca_certificate_sign(args.ca, args.sign, args.install)
+ generate_ca_certificate_sign(args.ca, args.sign, install=args.install, file=args.file)
else:
- generate_ca_certificate(args.ca, args.install)
+ generate_ca_certificate(args.ca, install=args.install, file=args.file)
elif args.certificate:
if args.sign:
- generate_certificate_sign(args.certificate, args.sign, args.install)
+ generate_certificate_sign(args.certificate, args.sign, install=args.install, file=args.file)
elif args.self_sign:
- generate_certificate_selfsign(args.certificate, args.install)
+ generate_certificate_selfsign(args.certificate, install=args.install, file=args.file)
else:
generate_certificate_request(name=args.certificate, install=args.install)
elif args.crl:
- generate_certificate_revocation_list(args.crl, args.install)
+ generate_certificate_revocation_list(args.crl, install=args.install, file=args.file)
elif args.ssh:
- generate_ssh_keypair(args.ssh, args.install)
+ generate_ssh_keypair(args.ssh, install=args.install, file=args.file)
elif args.dh:
- generate_dh_parameters(args.dh, args.install)
+ generate_dh_parameters(args.dh, install=args.install, file=args.file)
elif args.keypair:
- generate_keypair(args.keypair, args.install)
+ generate_keypair(args.keypair, install=args.install, file=args.file)
elif args.openvpn:
- generate_openvpn_key(args.openvpn, args.install)
+ generate_openvpn_key(args.openvpn, install=args.install, file=args.file)
elif args.wireguard:
if args.key:
- generate_wireguard_key(args.key, args.install)
+ generate_wireguard_key(args.key, install=args.install, file=args.file)
elif args.psk:
- generate_wireguard_psk(args.psk, args.install)
+ generate_wireguard_psk(args.psk, install=args.install, file=args.file)
elif args.action == 'show':
if args.ca:
show_certificate_authority(None if args.ca == 'all' else args.ca)
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index ff1e3cc56..4df275e04 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.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
@@ -27,8 +27,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
from vyos.config import Config
-from vyos.util import call
-
+from vyos.util import is_systemd_service_running
lease_file = "/config/dhcpd.leases"
pool_key = "shared-networkname"
@@ -217,7 +216,7 @@ if __name__ == '__main__':
exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if call('systemctl -q is-active isc-dhcp-server.service') != 0:
+ if not is_systemd_service_running('isc-dhcp-server.service'):
print("WARNING: DHCP server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
index f70f04298..1f987ff7b 100755
--- a/src/op_mode/show_dhcpv6.py
+++ b/src/op_mode/show_dhcpv6.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
@@ -27,7 +27,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
from vyos.config import Config
-from vyos.util import call
+from vyos.util import is_systemd_service_running
lease_file = "/config/dhcpdv6.leases"
pool_key = "shared-networkname"
@@ -202,7 +202,7 @@ if __name__ == '__main__':
exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if call('systemctl -q is-active isc-dhcp-server6.service') != 0:
+ if not is_systemd_service_running('isc-dhcp-server6.service'):
print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
index 94358c6e4..3c7a90205 100755
--- a/src/op_mode/show_vrf.py
+++ b/src/op_mode/show_vrf.py
@@ -20,12 +20,11 @@ from json import loads
from vyos.util import cmd
-vrf_out_tmpl = """
-VRF name state mac address flags interfaces
+vrf_out_tmpl = """VRF name state mac address flags interfaces
-------- ----- ----------- ----- ----------
-{% for v in vrf %}
+{%- for v in vrf %}
{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
-{% endfor %}
+{%- endfor %}
"""
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
deleted file mode 100755
index e08bc983a..000000000
--- a/src/op_mode/wireguard.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 argparse
-import os
-import sys
-import shutil
-import syslog as sl
-import re
-
-from vyos.config import Config
-from vyos.ifconfig import WireGuardIf
-from vyos.util import cmd
-from vyos.util import run
-from vyos.util import check_kmod
-from vyos import ConfigError
-
-dir = r'/config/auth/wireguard'
-psk = dir + '/preshared.key'
-
-k_mod = 'wireguard'
-
-def generate_keypair(pk, pub):
- """ generates a keypair which is stored in /config/auth/wireguard """
- old_umask = os.umask(0o027)
- if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0:
- raise ConfigError("wireguard key-pair generation failed")
- else:
- sl.syslog(
- sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
- os.umask(old_umask)
-
-
-def genkey(location):
- """ helper function to check, regenerate the keypair """
- pk = "{}/private.key".format(location)
- pub = "{}/public.key".format(location)
- old_umask = os.umask(0o027)
- if os.path.exists(pk) and os.path.exists(pub):
- try:
- choice = input(
- "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
- if choice == 'y' or choice == 'Y':
- generate_keypair(pk, pub)
- except KeyboardInterrupt:
- sys.exit(0)
- else:
- """ if keypair is bing executed from a running iso """
- if not os.path.exists(location):
- run(f'sudo mkdir -p {location}')
- run(f'sudo chgrp vyattacfg {location}')
- run(f'sudo chmod 750 {location}')
- generate_keypair(pk, pub)
- os.umask(old_umask)
-
-
-def showkey(key):
- """ helper function to show privkey or pubkey """
- if os.path.exists(key):
- print (open(key).read().strip())
- else:
- print ("{} not found".format(key))
-
-
-def genpsk():
- """
- generates a preshared key and shows it on stdout,
- it's stored only in the cli config
- """
-
- psk = cmd('wg genpsk')
- print(psk)
-
-def list_key_dirs():
- """ lists all dirs under /config/auth/wireguard """
- if os.path.exists(dir):
- nks = next(os.walk(dir))[1]
- for nk in nks:
- print (nk)
-
-def del_key_dir(kname):
- """ deletes /config/auth/wireguard/<kname> """
- kdir = "{0}/{1}".format(dir,kname)
- if not os.path.isdir(kdir):
- print ("named keypair {} not found".format(kname))
- return 1
- shutil.rmtree(kdir)
-
-
-if __name__ == '__main__':
- check_kmod(k_mod)
- parser = argparse.ArgumentParser(description='wireguard key management')
- parser.add_argument(
- '--genkey', action="store_true", help='generate key-pair')
- parser.add_argument(
- '--showpub', action="store_true", help='shows public key')
- parser.add_argument(
- '--showpriv', action="store_true", help='shows private key')
- parser.add_argument(
- '--genpsk', action="store_true", help='generates preshared-key')
- parser.add_argument(
- '--location', action="store", help='key location within {}'.format(dir))
- parser.add_argument(
- '--listkdir', action="store_true", help='lists named keydirectories')
- parser.add_argument(
- '--delkdir', action="store_true", help='removes named keydirectories')
- parser.add_argument(
- '--showinterface', action="store", help='shows interface details')
- args = parser.parse_args()
-
- try:
- if args.genkey:
- if args.location:
- genkey("{0}/{1}".format(dir, args.location))
- else:
- genkey("{}/default".format(dir))
- if args.showpub:
- if args.location:
- showkey("{0}/{1}/public.key".format(dir, args.location))
- else:
- showkey("{}/default/public.key".format(dir))
- if args.showpriv:
- if args.location:
- showkey("{0}/{1}/private.key".format(dir, args.location))
- else:
- showkey("{}/default/private.key".format(dir))
- if args.genpsk:
- genpsk()
- if args.listkdir:
- list_key_dirs()
- if args.showinterface:
- try:
- intf = WireGuardIf(args.showinterface, create=False, debug=False)
- print(intf.operational.show_interface())
- # the interface does not exists
- except Exception:
- pass
- if args.delkdir:
- if args.location:
- del_key_dir(args.location)
- else:
- del_key_dir("default")
-
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py
index 7a620a01e..7661254da 100755
--- a/src/op_mode/wireguard_client.py
+++ b/src/op_mode/wireguard_client.py
@@ -38,7 +38,7 @@ To enable this configuration on a VyOS router you can use the following commands
{% for addr in address if address is defined %}
set interfaces wireguard {{ interface }} peer {{ name }} allowed-ips '{{ addr }}'
{% endfor %}
-set interfaces wireguard {{ interface }} peer {{ name }} pubkey '{{ pubkey }}'
+set interfaces wireguard {{ interface }} peer {{ name }} public-key '{{ pubkey }}'
"""
client_config = """
diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
new file mode 100644
index 000000000..a04138010
--- /dev/null
+++ b/src/services/api/graphql/README.graphql
@@ -0,0 +1,116 @@
+
+Example using GraphQL mutations to configure a DHCP server:
+
+This assumes that the http-api is running:
+
+'set service https api'
+
+One can configure an address on an interface, and configure the DHCP server
+to run with that address as default router by requesting these 'mutations'
+in the GraphQL playground:
+
+mutation {
+ createInterfaceEthernet (data: {interface: "eth1",
+ address: "192.168.0.1/24",
+ description: "BOB"}) {
+ success
+ errors
+ data {
+ address
+ }
+ }
+}
+
+mutation {
+ createDhcpServer(data: {sharedNetworkName: "BOB",
+ subnet: "192.168.0.0/24",
+ defaultRouter: "192.168.0.1",
+ dnsServer: "192.168.0.1",
+ domainName: "vyos.net",
+ lease: 86400,
+ range: 0,
+ start: "192.168.0.9",
+ stop: "192.168.0.254",
+ dnsForwardingAllowFrom: "192.168.0.0/24",
+ dnsForwardingCacheSize: 0,
+ dnsForwardingListenAddress: "192.168.0.1"}) {
+ success
+ errors
+ data {
+ defaultRouter
+ }
+ }
+}
+
+The GraphQL playground will be found at:
+
+https://{{ host_address }}/graphql
+
+An equivalent curl command to the first example above would be:
+
+curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
+
+Note that the 'mutation' term is prefaced by 'query' in the curl command.
+
+What's here:
+
+services
+├── api
+│   └── graphql
+│   ├── graphql
+│   │   ├── directives.py
+│   │   ├── __init__.py
+│   │   ├── mutations.py
+│   │   └── schema
+│   │   ├── dhcp_server.graphql
+│   │   ├── interface_ethernet.graphql
+│   │   └── schema.graphql
+│   ├── recipes
+│   │   ├── dhcp_server.py
+│   │   ├── __init__.py
+│   │   ├── interface_ethernet.py
+│   │   ├── recipe.py
+│   │   └── templates
+│   │   ├── dhcp_server.tmpl
+│   │   └── interface_ethernet.tmpl
+│   └── state.py
+├── vyos-configd
+├── vyos-hostsd
+└── vyos-http-api-server
+
+The GraphQL library that we are using, Ariadne, advertises itself as a
+'schema-first' implementation: define the schema; define resolvers
+(handlers) for declared Query and Mutation types (Subscription types are not
+currently used).
+
+In the current approach to a high-level API, we consider the
+Jinja2-templated collection of configuration mode 'set'/'delete' commands as
+the Ur-data; the GraphQL schema is produced from those files, located in
+'api/graphql/recipes/templates'.
+
+Resolvers for the schema Mutation fields are dynamically generated using a
+'directive' added to the respective schema field. The directive,
+'@generate', is handled by the class 'DataDirective' in
+'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in
+'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate
+wrapper in 'api/graphql/recipes', with base class doing the (overridable)
+configuration steps of calling all defined 'set'/'delete' commands.
+
+Integrating the above with vyos-http-api-server is ~10 lines of code.
+
+What needs to be done:
+
+• automate generation of schema and wrappers from templated configuration
+commands
+
+• investigate whether the subclassing provided by the named wrappers in
+'api/graphql/recipes' is sufficient for use cases which need to modify data
+
+• encapsulate the manipulation of 'canonical names' which transforms the
+prefixed camel-case schema names to various snake-case file/function names
+
+• consider mechanism for migration of templates: offline vs. on-the-fly
+
+• define the naming convention for those schema fields that refer to
+configuration mode parameters: e.g. how much of the path is needed as prefix
+to uniquely define the term
diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/graphql/__init__.py
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
new file mode 100644
index 000000000..651421c35
--- /dev/null
+++ b/src/services/api/graphql/graphql/directives.py
@@ -0,0 +1,17 @@
+from ariadne import SchemaDirectiveVisitor, ObjectType
+from . mutations import make_resolver
+
+class DataDirective(SchemaDirectiveVisitor):
+ """
+ Class providing implementation of 'generate' directive in schema.
+
+ """
+ def visit_field_definition(self, field, object_type):
+ name = f'{field.type}'
+ # field.type contains the return value of the mutation; trim value
+ # to produce canonical name
+ name = name.replace('Result', '', 1)
+
+ func = make_resolver(name)
+ field.resolve = func
+ return field
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
new file mode 100644
index 000000000..98c665c9a
--- /dev/null
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -0,0 +1,60 @@
+
+from importlib import import_module
+from typing import Any, Dict
+from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
+from graphql import GraphQLResolveInfo
+from makefun import with_signature
+
+from .. import state
+
+mutation = ObjectType("Mutation")
+
+def make_resolver(mutation_name):
+ """Dynamically generate a resolver for the mutation named in the
+ schema by 'mutation_name'.
+
+ Dynamic generation is provided using the package 'makefun' (via the
+ decorator 'with_signature'), which provides signature-preserving
+ function wrappers; it provides several improvements over, say,
+ functools.wraps.
+
+ :raise Exception:
+ encapsulating ConfigErrors, or internal errors
+ """
+ class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+ func_base_name = convert_camel_case_to_snake(class_name)
+ resolver_name = f'resolve_create_{func_base_name}'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+ @mutation.field(mutation_name)
+ @convert_kwargs_to_snake_case
+ @with_signature(func_sig, func_name=resolver_name)
+ async def func_impl(*args, **kwargs):
+ try:
+ if 'data' not in kwargs:
+ return {
+ "success": False,
+ "errors": ['missing data']
+ }
+
+ data = kwargs['data']
+ session = state.settings['app'].state.vyos_session
+
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ k = klass(session, data)
+ k.configure()
+
+ return {
+ "success": True,
+ "data": data
+ }
+ except Exception as error:
+ return {
+ "success": False,
+ "errors": [str(error)]
+ }
+
+ return func_impl
+
+
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
new file mode 100644
index 000000000..a7ee75d40
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -0,0 +1,35 @@
+input dhcpServerConfigInput {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ dnsServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type dhcpServerConfig {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ dnsServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type createDhcpServerResult {
+ data: dhcpServerConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
new file mode 100644
index 000000000..fdcf97bad
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -0,0 +1,18 @@
+input interfaceEthernetConfigInput {
+ interface: String
+ address: String
+ replace: Boolean = true
+ description: String
+}
+
+type interfaceEthernetConfig {
+ interface: String
+ address: String
+ description: String
+}
+
+type createInterfaceEthernetResult {
+ data: interfaceEthernetConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
new file mode 100644
index 000000000..8a5e17962
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -0,0 +1,15 @@
+schema {
+ query: Query
+ mutation: Mutation
+}
+
+type Query {
+ _dummy: String
+}
+
+directive @generate on FIELD_DEFINITION
+
+type Mutation {
+ createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
+ createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+}
diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/recipes/__init__.py
diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
new file mode 100644
index 000000000..3edb3028e
--- /dev/null
+++ b/src/services/api/graphql/recipes/dhcp_server.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class DhcpServer(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
new file mode 100644
index 000000000..f88f5924f
--- /dev/null
+++ b/src/services/api/graphql/recipes/interface_ethernet.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class InterfaceEthernet(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
new file mode 100644
index 000000000..8fbb9e0bf
--- /dev/null
+++ b/src/services/api/graphql/recipes/recipe.py
@@ -0,0 +1,49 @@
+from ariadne import convert_camel_case_to_snake
+import vyos.defaults
+from vyos.template import render
+
+class Recipe(object):
+ def __init__(self, session, data):
+ self._session = session
+ self.data = data
+ self._name = convert_camel_case_to_snake(type(self).__name__)
+
+ @property
+ def data(self):
+ return self.__data
+
+ @data.setter
+ def data(self, data):
+ if isinstance(data, dict):
+ self.__data = data
+ else:
+ raise ValueError("data must be of type dict")
+
+ def configure(self):
+ session = self._session
+ data = self.data
+ func_base_name = self._name
+
+ tmpl_file = f'{func_base_name}.tmpl'
+ cmd_file = f'/tmp/{func_base_name}.cmds'
+ tmpl_dir = vyos.defaults.directories['api_templates']
+
+ try:
+ render(cmd_file, tmpl_file, data, location=tmpl_dir)
+ commands = []
+ with open(cmd_file) as f:
+ lines = f.readlines()
+ for line in lines:
+ commands.append(line.split())
+ for cmd in commands:
+ if cmd[0] == 'set':
+ session.set(cmd[1:])
+ elif cmd[0] == 'delete':
+ session.delete(cmd[1:])
+ else:
+ raise ValueError('Operation must be "set" or "delete"')
+ session.commit()
+ except Exception as error:
+ raise error
+
+
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
new file mode 100644
index 000000000..629ce83c1
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
@@ -0,0 +1,9 @@
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }}
+set service dns forwarding allow-from {{ dns_forwarding_allow_from }}
+set service dns forwarding cache-size {{ dns_forwarding_cache_size }}
+set service dns forwarding listen-address {{ dns_forwarding_listen_address }}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
new file mode 100644
index 000000000..d9d7ed691
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
@@ -0,0 +1,5 @@
+{% if replace %}
+delete interfaces ethernet {{ interface }} address
+{% endif %}
+set interfaces ethernet {{ interface }} address {{ address }}
+set interfaces ethernet {{ interface }} description {{ description }}
diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py
new file mode 100644
index 000000000..63db9f4ef
--- /dev/null
+++ b/src/services/api/graphql/state.py
@@ -0,0 +1,4 @@
+
+def init():
+ global settings
+ settings = {}
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 6f770b696..670b6e66a 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -133,8 +133,7 @@ def explicit_print(path, mode, msg):
logger.critical("error explicit_print")
def run_script(script, config, args) -> int:
- if args:
- script.argv = args
+ script.argv = args
config.set_level([])
try:
c = script.get_config(config)
@@ -208,7 +207,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
script_name = None
- args = None
+ args = []
res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
if res.group(1):
@@ -221,7 +220,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
if res.group(3):
args = res.group(3).split()
- args.insert(0, f'{script_name}.py')
+ args.insert(0, f'{script_name}.py')
if script_name not in include_set:
return R_PASS
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index cbf321dc8..cb4ce4072 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -36,10 +36,16 @@ from starlette.datastructures import FormData, MutableHeaders
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
+from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+from ariadne.asgi import GraphQL
+
import vyos.config
+import vyos.defaults
from vyos.configsession import ConfigSession, ConfigSessionError
+import api.graphql.state
+
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
@@ -603,6 +609,25 @@ def show_op(data: ShowModel):
return success(res)
+###
+# GraphQL integration
+###
+
+api.graphql.state.init()
+
+from api.graphql.graphql.mutations import mutation
+from api.graphql.graphql.directives import DataDirective
+
+api_schema_dir = vyos.defaults.directories['api_schema']
+
+type_defs = load_schema_from_path(api_schema_dir)
+
+schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective})
+
+app.add_route('/graphql', GraphQL(schema, debug=True))
+
+###
+
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
# else no one else will be able to commit
@@ -626,6 +651,8 @@ if __name__ == '__main__':
app.state.vyos_debug = True if server_config['debug'] == 'true' else False
app.state.vyos_strict = True if server_config['strict'] == 'true' else False
+ api.graphql.state.settings['app'] = app
+
try:
uvicorn.run(app, host=server_config["listen_address"],
port=int(server_config["port"]),
diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service
index 9aa70a7cc..a7d86e69c 100644
--- a/src/systemd/isc-dhcp-server.service
+++ b/src/systemd/isc-dhcp-server.service
@@ -14,10 +14,10 @@ Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhc
PIDFile=/run/dhcp-server/dhcpd.pid
ExecStartPre=/bin/sh -ec '\
touch ${LEASE_FILE}; \
-chown dhcpd:nogroup ${LEASE_FILE}* ; \
+chown dhcpd:vyattacfg ${LEASE_FILE}* ; \
chmod 664 ${LEASE_FILE}* ; \
-/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
-ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
Restart=always
[Install]
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
index c78a80776..29167c635 100755
--- a/src/validators/vrf-name
+++ b/src/validators/vrf-name
@@ -33,8 +33,8 @@ if __name__ == '__main__':
if vrf == "lo":
exit(1)
- pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \
- "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan)\d+(\.\d+(v.+)?)?$).*$"
+ pattern = r'^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|\
+ vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan|\d)\d*(\.\d+)?(v.+)?).*$'
if not re.match(pattern, vrf):
exit(1)