summaryrefslogtreecommitdiff
path: root/src/conf_mode/interfaces-openvpn.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/interfaces-openvpn.py')
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py417
1 files changed, 127 insertions, 290 deletions
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 6bd269e97..f34e4f7fe 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -17,262 +17,27 @@
import os
import re
-from jinja2 import Template
+from jinja2 import FileSystemLoader, Environment
from copy import deepcopy
from sys import exit
from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH
from grp import getgrnam
from ipaddress import ip_address,ip_network,IPv4Interface
from netifaces import interfaces
-from psutil import pid_exists
from pwd import getpwnam
-from subprocess import Popen, PIPE
from time import sleep
+from shutil import rmtree
-from vyos import ConfigError
from vyos.config import Config
-from vyos.ifconfig import Interface
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import VTunIf
+from vyos.util import process_running, cmd, is_bridge_member
from vyos.validate import is_addr_assigned
+from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by interfaces-openvpn.py ###
-#
-# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
-# for individual keyword definition
-
-{% if description %}
-# {{ description }}
-{% endif %}
-
-verb 3
-status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30
-writepid /var/run/openvpn/{{ intf }}.pid
-
-dev-type {{ type }}
-dev {{ intf }}
-user {{ uid }}
-group {{ gid }}
-persist-key
-iproute /usr/libexec/vyos/system/unpriv-ip
-
-proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
-
-{%- if local_host %}
-local {{ local_host }}
-{% endif %}
-
-{%- if local_port %}
-lport {{ local_port }}
-{% endif %}
-
-{%- if remote_port %}
-rport {{ remote_port }}
-{% endif %}
-
-{%- if remote_host %}
-{% for remote in remote_host -%}
-remote {{ remote }}
-{% endfor -%}
-{% endif %}
-
-{%- if shared_secret_file %}
-secret {{ shared_secret_file }}
-{% endif %}
-
-{%- if persistent_tunnel %}
-persist-tun
-{% endif %}
-
-{%- if mode %}
-{%- if 'client' in mode %}
-#
-# OpenVPN Client mode
-#
-client
-nobind
-{%- elif 'server' in mode %}
-#
-# OpenVPN Server mode
-#
-mode server
-tls-server
-keepalive {{ ping_interval }} {{ ping_restart }}
-management /tmp/openvpn-mgmt-intf unix
-
-{%- if server_topology %}
-topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %}
-{% endif %}
-
-{% for ns in server_dns_nameserver -%}
-push "dhcp-option DNS {{ ns }}"
-{% endfor -%}
-
-{% for route in server_push_route -%}
-push "route {{ route }}"
-{% endfor -%}
-
-{%- if server_domain %}
-push "dhcp-option DOMAIN {{ server_domain }}"
-{% endif %}
-
-{%- if server_max_conn %}
-max-clients {{ server_max_conn }}
-{% endif %}
-
-{%- if bridge_member %}
-server-bridge nogw
-{%- else %}
-server {{ server_subnet }}
-{% endif %}
-
-{%- if server_reject_unconfigured %}
-ccd-exclusive
-{% endif %}
-
-{%- else %}
-#
-# OpenVPN site-2-site mode
-#
-ping {{ ping_interval }}
-ping-restart {{ ping_restart }}
-
-{%- if local_address_subnet %}
-ifconfig {{ local_address }} {{ local_address_subnet }}
-{% elif remote_address %}
-ifconfig {{ local_address }} {{ remote_address }}
-{% endif %}
-
-{% endif %}
-{% endif %}
-
-{%- if tls_ca_cert %}
-ca {{ tls_ca_cert }}
-{% endif %}
-
-{%- if tls_cert %}
-cert {{ tls_cert }}
-{% endif %}
-
-{%- if tls_key %}
-key {{ tls_key }}
-{% endif %}
-
-{%- if tls_crl %}
-crl-verify {{ tls_crl }}
-{% endif %}
-
-{%- if tls_version_min %}
-tls-version-min {{tls_version_min}}
-{% endif %}
-
-{%- if tls_dh %}
-dh {{ tls_dh }}
-{% endif %}
-
-{%- if tls_auth %}
-tls-auth {{tls_auth}}
-{% endif %}
-
-{%- if 'active' in tls_role %}
-tls-client
-{%- elif 'passive' in tls_role %}
-tls-server
-{% endif %}
-
-{%- if redirect_gateway %}
-push "redirect-gateway {{ redirect_gateway }}"
-{% endif %}
-
-{%- if compress_lzo %}
-compress lzo
-{% endif %}
-
-{%- if hash %}
-auth {{ hash }}
-{% endif %}
-
-{%- if encryption %}
-{%- if 'des' in encryption %}
-cipher des-cbc
-{%- elif '3des' in encryption %}
-cipher des-ede3-cbc
-{%- elif 'bf128' in encryption %}
-cipher bf-cbc
-keysize 128
-{%- elif 'bf256' in encryption %}
-cipher bf-cbc
-keysize 25
-{%- elif 'aes128gcm' in encryption %}
-cipher aes-128-gcm
-{%- elif 'aes128' in encryption %}
-cipher aes-128-cbc
-{%- elif 'aes192gcm' in encryption %}
-cipher aes-192-gcm
-{%- elif 'aes192' in encryption %}
-cipher aes-192-cbc
-{%- elif 'aes256gcm' in encryption %}
-cipher aes-256-gcm
-{%- elif 'aes256' in encryption %}
-cipher aes-256-cbc
-{% endif %}
-{% endif %}
-
-{%- if ncp_ciphers %}
-ncp-ciphers {{ncp_ciphers}}
-{% endif %}
-{%- if disable_ncp %}
-ncp-disable
-{% endif %}
-
-{%- if auth %}
-auth-user-pass /tmp/openvpn-{{ intf }}-pw
-auth-retry nointeract
-{% endif %}
-
-{%- if client %}
-client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }}
-{% endif %}
-
-# DEPRECATED This option will be removed in OpenVPN 2.5
-# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this:
-# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old
-# behaviour was to remap any character other than alphanumeric, underscore ('_'),
-# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject
-# string as returned by the tls_id environmental variable, could additionally
-# contain colon (':') or equal ('='). When using the --compat-names option, this
-# old formatting and remapping will be re-enabled again. This is purely implemented
-# for compatibility reasons when using older plug-ins or scripts which does not
-# handle the new formatting or UTF-8 characters.
-#
-# See https://phabricator.vyos.net/T1512
-compat-names
-
-{% for option in options -%}
-{{ option }}
-{% endfor -%}
-"""
-
-client_tmpl = """
-### Autogenerated by interfaces-openvpn.py ###
-
-ifconfig-push {{ ip }} {{ remote_netmask }}
-{% for route in push_route -%}
-push "route {{ route }}"
-{% endfor -%}
-
-{% for net in subnet -%}
-iroute {{ net }}
-{% endfor -%}
-
-{%- if disable %}
-disable
-{% endif %}
-"""
-
default_config_data = {
'address': [],
'auth_user': '',
@@ -287,6 +52,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'ping_restart': '60',
'ping_interval': '10',
'local_address': '',
@@ -297,7 +66,7 @@ default_config_data = {
'ncp_ciphers': '',
'options': [],
'persistent_tunnel': False,
- 'protocol': '',
+ 'protocol': 'udp',
'redirect_gateway': '',
'remote_address': '',
'remote_host': [],
@@ -318,6 +87,7 @@ default_config_data = {
'tls_crl': '',
'tls_dh': '',
'tls_key': '',
+ 'tls_crypt': '',
'tls_role': '',
'tls_version_min': '',
'type': 'tun',
@@ -325,9 +95,6 @@ default_config_data = {
'gid': group,
}
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
def get_config_name(intf):
cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf)
@@ -360,7 +127,7 @@ def fixup_permission(filename, permission=S_IRUSR):
def checkCertHeader(header, filename):
"""
Verify if filename contains specified header.
- Returns True on success or on file not found to not trigger the exceptions
+ Returns True if match is found, False if no match or file is not found
"""
if not os.path.isfile(filename):
return False
@@ -370,17 +137,17 @@ def checkCertHeader(header, filename):
if re.match(header, line):
return True
- return True
+ return False
def get_config():
openvpn = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
@@ -482,10 +249,25 @@ def get_config():
if conf.exists('local-port'):
openvpn['local_port'] = conf.return_value('local-port')
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ openvpn['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ openvpn['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# OpenVPN operation mode
if conf.exists('mode'):
- mode = conf.return_value('mode')
- openvpn['mode'] = mode
+ openvpn['mode'] = conf.return_value('mode')
# Additional OpenVPN options
if conf.exists('openvpn-option'):
@@ -633,6 +415,11 @@ def get_config():
openvpn['tls_key'] = conf.return_value('tls key-file')
openvpn['tls'] = True
+ # File containing key to encrypt control channel packets
+ if conf.exists('tls crypt-file'):
+ openvpn['tls_crypt'] = conf.return_value('tls crypt-file')
+ openvpn['tls'] = True
+
# Role in TLS negotiation
if conf.exists('tls role'):
openvpn['tls_role'] = conf.return_value('tls role')
@@ -641,6 +428,7 @@ def get_config():
# Minimum required TLS version
if conf.exists('tls tls-version-min'):
openvpn['tls_version_min'] = conf.return_value('tls tls-version-min')
+ openvpn['tls'] = True
if conf.exists('shared-secret-key-file'):
openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
@@ -648,12 +436,25 @@ def get_config():
if conf.exists('use-lzo-compression'):
openvpn['compress_lzo'] = True
+ # Special case when using EC certificates:
+ # if key-file is EC and dh-file is unset, set tls_dh to 'none'
+ if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ openvpn['tls_dh'] = 'none'
+
return openvpn
def verify(openvpn):
if openvpn['deleted']:
+ interface = openvpn['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
+
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
@@ -682,7 +483,7 @@ def verify(openvpn):
if not openvpn['remote_host']:
raise ConfigError('Must specify "remote-host" in client mode')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" in client mode')
#
@@ -732,8 +533,8 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
- if not openvpn['tls_dh']:
- raise ConfigError('Must specify "tls dh-file" in server mode')
+ if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
if not openvpn['server_subnet']:
if not openvpn['bridge_member']:
@@ -800,6 +601,9 @@ def verify(openvpn):
if not openvpn['tls_key']:
raise ConfigError('Must specify "tls key-file"')
+ if openvpn['tls_auth'] and openvpn['tls_crypt']:
+ raise ConfigError('TLS auth and crypt are mutually exclusive')
+
if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
@@ -812,14 +616,18 @@ def verify(openvpn):
raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
if openvpn['tls_key']:
- if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']):
+ if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
+ if openvpn['tls_crypt']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']):
+ raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt']))
+
if openvpn['tls_crl']:
if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
@@ -832,7 +640,7 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
elif openvpn['tls_role'] == 'passive':
@@ -842,6 +650,12 @@ def verify(openvpn):
if not openvpn['tls_dh']:
raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+ if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH')
+ else:
+ print('Diffie-Hellman prime file is unspecified, assuming ECDH')
+
#
# Auth user/pass
#
@@ -857,20 +671,27 @@ def verify(openvpn):
#
subnet = openvpn['server_subnet'].replace(' ', '/')
for client in openvpn['client']:
- if not ip_address(client['ip']) in ip_network(subnet):
+ if client['ip'] and not ip_address(client['ip']) in ip_network(subnet):
raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet))
-
-
return None
def generate(openvpn):
if openvpn['deleted'] or openvpn['disable']:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
+ # we can't know which clients were deleted, remove all client configs
+ if os.path.isdir(os.path.join(directory, 'ccd', interface)):
+ rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True)
+
# create config directory on demand
openvpn_mkdir(directory)
# create status directory on demand
@@ -892,6 +713,11 @@ def generate(openvpn):
fixup_permission(auth_file)
+ else:
+ # delete old auth file if present
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)):
+ os.remove('/tmp/openvpn-{}-pw'.format(interface))
+
# get numeric uid/gid
uid = getpwnam(user).pw_uid
gid = getgrnam(group).gr_gid
@@ -899,19 +725,17 @@ def generate(openvpn):
# Generate client specific configuration
for client in openvpn['client']:
client_file = directory + '/ccd/' + interface + '/' + client['name']
- tmpl = Template(client_tmpl)
+ tmpl = env.get_template('client.conf.tmpl')
client_text = tmpl.render(client)
with open(client_file, 'w') as f:
f.write(client_text)
os.chown(client_file, uid, gid)
- tmpl = Template(config_tmpl)
+ tmpl = env.get_template('server.conf.tmpl')
config_text = tmpl.render(openvpn)
-
# we need to support quoting of raw parameters from OpenVPN CLI
# see https://phabricator.vyos.net/T1632
config_text = config_text.replace(""",'"')
-
with open(get_config_name(interface), 'w') as f:
f.write(config_text)
os.chown(get_config_name(interface), uid, gid)
@@ -919,20 +743,18 @@ def generate(openvpn):
return None
def apply(openvpn):
- pid = 0
pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
# Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the
# service as the configuration is not re-read. Stop daemon only if it's
# running - it could have died or killed by someone evil
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
# cleanup old PID file
if os.path.isfile(pidfile):
@@ -946,11 +768,12 @@ def apply(openvpn):
# cleanup client config dir
directory = os.path.dirname(get_config_name(openvpn['intf']))
- if os.path.isdir(directory + '/ccd/' + openvpn['intf']):
- try:
- os.remove(directory + '/ccd/' + openvpn['intf'] + '/*')
- except:
- pass
+ if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])):
+ rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True)
+
+ # cleanup auth file
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])):
+ os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf']))
return None
@@ -962,16 +785,19 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --pidfile ' + pidfile
- cmd += ' --exec /usr/sbin/openvpn'
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ command += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
- cmd += ' --'
- cmd += ' --daemon openvpn-' + openvpn['intf']
- cmd += ' --config ' + get_config_name(openvpn['intf'])
+ command += ' --'
+ command += ' --daemon openvpn-' + openvpn['intf']
+ command += ' --config ' + get_config_name(openvpn['intf'])
# execute assembled command
- subprocess_cmd(cmd)
+ cmd(command)
# better late then sorry ... but we can only set interface alias after
# OpenVPN has been launched and created the interface
@@ -991,14 +817,25 @@ def apply(openvpn):
try:
# we need to catch the exception if the interface is not up due to
# reason stated above
- Interface(openvpn['intf']).set_alias(openvpn['description'])
+ o = VTunIf(openvpn['intf'])
+ # update interface description used e.g. within SNMP
+ o.set_alias(openvpn['description'])
+ # IPv6 address autoconfiguration
+ o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+
except:
pass
# TAP interface needs to be brought up explicitly
if openvpn['type'] == 'tap':
if not openvpn['disable']:
- Interface(openvpn['intf']).set_state('up')
+ VTunIf(openvpn['intf']).set_admin_state('up')
return None