#!/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 . import os import re from jinja2 import Template 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 vyos import ConfigError from vyos.config import Config from vyos.ifconfig import VTunIf from vyos.validate import is_addr_assigned 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_crypt %} tls-crypt {{ tls_crypt }} {% 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': '', 'auth_pass': '', 'auth': False, 'bridge_member': [], 'compress_lzo': False, 'deleted': False, 'description': '', 'disable': False, 'disable_ncp': False, 'encryption': '', 'hash': '', 'intf': '', 'ping_restart': '60', 'ping_interval': '10', 'local_address': '', 'local_address_subnet': '', 'local_host': '', 'local_port': '', 'mode': '', 'ncp_ciphers': '', 'options': [], 'persistent_tunnel': False, 'protocol': '', 'redirect_gateway': '', 'remote_address': '', 'remote_host': [], 'remote_port': '', 'client': [], 'server_domain': '', 'server_max_conn': '', 'server_dns_nameserver': [], 'server_push_route': [], 'server_reject_unconfigured': False, 'server_subnet': '', 'server_topology': '', 'shared_secret_file': '', 'tls': False, 'tls_auth': '', 'tls_ca_cert': '', 'tls_cert': '', 'tls_crl': '', 'tls_dh': '', 'tls_key': '', 'tls_crypt': '', 'tls_role': '', 'tls_version_min': '', 'type': 'tun', 'uid': user, '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) return cfg_file def openvpn_mkdir(directory): # create directory on demand if not os.path.exists(directory): os.mkdir(directory) # fix permissions - corresponds to mode 755 os.chmod(directory, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid os.chown(directory, uid, gid) def fixup_permission(filename, permission=S_IRUSR): """ Check if the given file exists and change ownershit to root/vyattacfg and appripriate file access permissions - default is user and group readable """ if os.path.isfile(filename): os.chmod(filename, permission) # make file owned by root / vyattacfg uid = getpwnam('root').pw_uid gid = getgrnam('vyattacfg').gr_gid os.chown(filename, uid, gid) 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(): openvpn = deepcopy(default_config_data) conf = Config() # determine tagNode instance 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']): openvpn['deleted'] = True return openvpn # Check if we belong to any bridge interface for bridge in conf.list_nodes('interfaces bridge'): for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): if intf == openvpn['intf']: openvpn['bridge_member'].append(intf) # set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf']) # retrieve authentication options - username if conf.exists('authentication username'): openvpn['auth_user'] = conf.return_value('authentication username') openvpn['auth'] = True # retrieve authentication options - username if conf.exists('authentication password'): openvpn['auth_pass'] = conf.return_value('authentication password') openvpn['auth'] = True # retrieve interface description if conf.exists('description'): openvpn['description'] = conf.return_value('description') # interface device-type if conf.exists('device-type'): openvpn['type'] = conf.return_value('device-type') # disable interface if conf.exists('disable'): openvpn['disable'] = True # data encryption algorithm cipher if conf.exists('encryption cipher'): openvpn['encryption'] = conf.return_value('encryption cipher') # disable ncp-ciphers support if conf.exists('encryption disable-ncp'): openvpn['disable_ncp'] = True # data encryption algorithm ncp-list if conf.exists('encryption ncp-ciphers'): _ncp_ciphers = [] for enc in conf.return_values('encryption ncp-ciphers'): if enc == 'des': _ncp_ciphers.append('des-cbc') _ncp_ciphers.append('DES-CBC') elif enc == '3des': _ncp_ciphers.append('des-ede3-cbc') _ncp_ciphers.append('DES-EDE3-CBC') elif enc == 'aes128': _ncp_ciphers.append('aes-128-cbc') _ncp_ciphers.append('AES-128-CBC') elif enc == 'aes128gcm': _ncp_ciphers.append('aes-128-gcm') _ncp_ciphers.append('AES-128-GCM') elif enc == 'aes192': _ncp_ciphers.append('aes-192-cbc') _ncp_ciphers.append('AES-192-CBC') elif enc == 'aes192gcm': _ncp_ciphers.append('aes-192-gcm') _ncp_ciphers.append('AES-192-GCM') elif enc == 'aes256': _ncp_ciphers.append('aes-256-cbc') _ncp_ciphers.append('AES-256-CBC') elif enc == 'aes256gcm': _ncp_ciphers.append('aes-256-gcm') _ncp_ciphers.append('AES-256-GCM') openvpn['ncp_ciphers'] = ':'.join(_ncp_ciphers) # hash algorithm if conf.exists('hash'): openvpn['hash'] = conf.return_value('hash') # Maximum number of keepalive packet failures if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'): fail_count = conf.return_value('keep-alive failure-count') interval = conf.return_value('keep-alive interval') openvpn['ping_interval' ] = interval openvpn['ping_restart' ] = int(interval) * int(fail_count) # Local IP address of tunnel - even as it is a tag node - we can only work # on the first address if conf.exists('local-address'): openvpn['local_address'] = conf.list_nodes('local-address')[0] if conf.exists('local-address {} subnet-mask'.format(openvpn['local_address'])): openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(openvpn['local_address'])) # Local IP address to accept connections if conf.exists('local-host'): openvpn['local_host'] = conf.return_value('local-host') # Local port number to accept connections if conf.exists('local-port'): openvpn['local_port'] = conf.return_value('local-port') # OpenVPN operation mode if conf.exists('mode'): mode = conf.return_value('mode') openvpn['mode'] = mode # Additional OpenVPN options if conf.exists('openvpn-option'): openvpn['options'] = conf.return_values('openvpn-option') # Do not close and reopen interface if conf.exists('persistent-tunnel'): openvpn['persistent_tunnel'] = True # Communication protocol if conf.exists('protocol'): openvpn['protocol'] = conf.return_value('protocol') # IP address of remote end of tunnel if conf.exists('remote-address'): openvpn['remote_address'] = conf.return_value('remote-address') # Remote host to connect to (dynamic if not set) if conf.exists('remote-host'): openvpn['remote_host'] = conf.return_values('remote-host') # Remote port number to connect to if conf.exists('remote-port'): openvpn['remote_port'] = conf.return_value('remote-port') # OpenVPN tunnel to be used as the default route # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ # redirect-gateway flags if conf.exists('replace-default-route'): openvpn['redirect_gateway'] = 'def1' if conf.exists('replace-default-route local'): openvpn['redirect_gateway'] = 'local def1' # Topology for clients if conf.exists('server topology'): openvpn['server_topology'] = conf.return_value('server topology') # Server-mode subnet (from which client IPs are allocated) if conf.exists('server subnet'): network = conf.return_value('server subnet') tmp = IPv4Interface(network).with_netmask # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template openvpn['server_subnet'] = tmp.replace(r'/', ' ') # Client-specific settings for client in conf.list_nodes('server client'): # set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client) data = { 'name': client, 'disable': False, 'ip': '', 'push_route': [], 'subnet': [], 'remote_netmask': '' } # note: with "topology subnet", this is " ". # with "topology p2p", this is " ". if openvpn['server_topology'] == 'subnet': # we are only interested in the netmask portion of server_subnet data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1] else: # we need the server subnet in format 192.0.2.0/255.255.255.0 subnet = openvpn['server_subnet'].replace(' ', r'/') # get iterator over the usable hosts in the network tmp = ip_network(subnet).hosts() # OpenVPN always uses the subnets first available IP address data['remote_netmask'] = list(tmp)[0] # Option to disable client connection if conf.exists('disable'): data['disable'] = True # IP address of the client if conf.exists('ip'): data['ip'] = conf.return_value('ip') # Route to be pushed to the client for network in conf.return_values('push-route'): tmp = IPv4Interface(network).with_netmask data['push_route'].append(tmp.replace(r'/', ' ')) # Subnet belonging to the client for network in conf.return_values('subnet'): tmp = IPv4Interface(network).with_netmask data['subnet'].append(tmp.replace(r'/', ' ')) # Append to global client list openvpn['client'].append(data) # re-set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf']) # DNS suffix to be pushed to all clients if conf.exists('server domain-name'): openvpn['server_domain'] = conf.return_value('server domain-name') # Number of maximum client connections if conf.exists('server max-connections'): openvpn['server_max_conn'] = conf.return_value('server max-connections') # Domain Name Server (DNS) if conf.exists('server name-server'): openvpn['server_dns_nameserver'] = conf.return_values('server name-server') # Route to be pushed to all clients if conf.exists('server push-route'): for network in conf.return_values('server push-route'): tmp = IPv4Interface(network).with_netmask openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) # Reject connections from clients that are not explicitly configured if conf.exists('server reject-unconfigured-clients'): openvpn['server_reject_unconfigured'] = True # File containing TLS auth static key if conf.exists('tls auth-file'): openvpn['tls_auth'] = conf.return_value('tls auth-file') openvpn['tls'] = True # File containing certificate for Certificate Authority (CA) if conf.exists('tls ca-cert-file'): openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file') openvpn['tls'] = True # File containing certificate for this host if conf.exists('tls cert-file'): openvpn['tls_cert'] = conf.return_value('tls cert-file') openvpn['tls'] = True # File containing certificate revocation list (CRL) for this host if conf.exists('tls crl-file'): openvpn['tls_crl'] = conf.return_value('tls crl-file') openvpn['tls'] = True # File containing Diffie Hellman parameters (server only) if conf.exists('tls dh-file'): openvpn['tls_dh'] = conf.return_value('tls dh-file') openvpn['tls'] = True # File containing this host's private key if conf.exists('tls key-file'): 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') openvpn['tls'] = True # Minimum required TLS version if conf.exists('tls tls-version-min'): openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') if conf.exists('shared-secret-key-file'): openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') 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']: return None if not openvpn['mode']: raise ConfigError('Must specify OpenVPN operation mode') # Checks which need to be performed on interface rmeoval if openvpn['deleted']: # OpenVPN interface can not be deleted if it's still member of a bridge if openvpn['bridge_member']: raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge)) # Check if we have disabled ncp and at the same time specified ncp-ciphers if openvpn['disable_ncp'] and openvpn['ncp_ciphers']: raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"') # # OpenVPN client mode - VERIFY # if openvpn['mode'] == 'client': if openvpn['local_port']: raise ConfigError('Cannot specify "local-port" in client mode') if openvpn['local_host']: raise ConfigError('Cannot specify "local-host" in client mode') if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Protocol "tcp-passive" is not valid in client mode') if not openvpn['remote_host']: raise ConfigError('Must specify "remote-host" in client mode') if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': raise ConfigError('Cannot specify "tls dh-file" in client mode') # # OpenVPN site-to-site - VERIFY # if openvpn['mode'] == 'site-to-site': if not (openvpn['local_address'] or openvpn['bridge_member']): raise ConfigError('Must specify "local-address" or "bridge member interface"') for host in openvpn['remote_host']: if host == openvpn['remote_address']: raise ConfigError('"remote-address" cannot be the same as "remote-host"') if openvpn['type'] == 'tun': if not openvpn['remote_address']: raise ConfigError('Must specify "remote-address"') if openvpn['local_address'] == openvpn['remote_address']: raise ConfigError('"local-address" and "remote-address" cannot be the same') if openvpn['local_address'] == openvpn['local_host']: raise ConfigError('"local-address" cannot be the same as "local-host"') if openvpn['ncp_ciphers']: raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') else: if openvpn['local_address'] or openvpn['remote_address']: raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode') elif openvpn['bridge_member']: raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode') # # OpenVPN server mode - VERIFY # if openvpn['mode'] == 'server': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Protocol "tcp-active" is not valid in server mode') if openvpn['remote_port']: raise ConfigError('Cannot specify "remote-port" in server mode') if openvpn['remote_host']: raise ConfigError('Cannot specify "remote-host" in server mode') 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'] 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']: raise ConfigError('Must specify "server subnet" option in server mode') else: # checks for both client and site-to-site go here if openvpn['server_reject_unconfigured']: raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode') if openvpn['server_topology']: raise ConfigError('The "topology" option is only valid in server mode') if (not openvpn['remote_host']) and openvpn['redirect_gateway']: raise ConfigError('Cannot set "replace-default-route" without "remote-host"') # # OpenVPN common verification section # not depending on any operation mode # # verify specified IP address is present on any interface on this system if openvpn['local_host']: if not is_addr_assigned(openvpn['local_host']): raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host'])) # TCP active if openvpn['protocol'] == 'tcp-active': if openvpn['local_port']: raise ConfigError('Cannot specify "local-port" with "tcp-active"') if not openvpn['remote_host']: raise ConfigError('Must specify "remote-host" with "tcp-active"') # shared secret and TLS if not (openvpn['shared_secret_file'] or openvpn['tls']): raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') if openvpn['shared_secret_file'] and openvpn['tls']: raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') if openvpn['mode'] in ['client', 'server']: if not openvpn['tls']: raise ConfigError('Must specify "tls" in client-server mode') # # TLS/encryption # if openvpn['shared_secret_file']: if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']: raise ConfigError('GCM encryption with shared-secret-key-file is not supported') if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']): raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file'])) if openvpn['tls']: if not openvpn['tls_ca_cert']: raise ConfigError('Must specify "tls ca-cert-file"') if not (openvpn['mode'] == 'client' and openvpn['auth']): if not openvpn['tls_cert']: raise ConfigError('Must specify "tls cert-file"') 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'])) if openvpn['tls_auth']: if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']): raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth'])) if openvpn['tls_cert']: if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']): raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) if 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'] 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'])) if openvpn['tls_role']: if openvpn['mode'] in ['client', 'server']: if not openvpn['tls_auth']: raise ConfigError('Cannot specify "tls role" in client-server mode') if openvpn['tls_role'] == 'active': if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') 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': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') 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 # if openvpn['auth']: if not openvpn['auth_user']: raise ConfigError('Username for authentication is missing') if not openvpn['auth_pass']: raise ConfigError('Password for authentication is missing') # # Client # subnet = openvpn['server_subnet'].replace(' ', '/') for client in openvpn['client']: if 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 interface = openvpn['intf'] directory = os.path.dirname(get_config_name(interface)) # create config directory on demand openvpn_mkdir(directory) # create status directory on demand openvpn_mkdir(directory + '/status') # create client config dir on demand openvpn_mkdir(directory + '/ccd') # crete client config dir per interface on demand openvpn_mkdir(directory + '/ccd/' + interface) # Fix file permissons for keys fixup_permission(openvpn['shared_secret_file']) fixup_permission(openvpn['tls_key']) # Generate User/Password authentication file if openvpn['auth']: auth_file = '/tmp/openvpn-{}-pw'.format(interface) with open(auth_file, 'w') as f: f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) fixup_permission(auth_file) # get numeric uid/gid uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid # Generate client specific configuration for client in openvpn['client']: client_file = directory + '/ccd/' + interface + '/' + client['name'] tmpl = Template(client_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) 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) 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' cmd += ' --stop ' cmd += ' --quiet' cmd += ' --oknodo' cmd += ' --pidfile ' + pidfile subprocess_cmd(cmd) # cleanup old PID file if os.path.isfile(pidfile): os.remove(pidfile) # Do some cleanup when OpenVPN is disabled/deleted if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file if os.path.isfile(get_config_name(openvpn['intf'])): os.remove(get_config_name(openvpn['intf'])) # 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 return None # On configuration change we need to wait for the 'old' interface to # vanish from the Kernel, if it is not gone, OpenVPN will report: # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) while openvpn['intf'] in interfaces(): sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process cmd = 'start-stop-daemon' cmd += ' --start ' cmd += ' --quiet' cmd += ' --oknodo' cmd += ' --pidfile ' + pidfile cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' cmd += ' --daemon openvpn-' + openvpn['intf'] cmd += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command subprocess_cmd(cmd) # better late then sorry ... but we can only set interface alias after # OpenVPN has been launched and created the interface cnt = 0 while openvpn['intf'] not in interfaces(): # If VPN tunnel can't be established because the peer/server isn't # (temporarily) available, the vtun interface never becomes registered # with the kernel, and the commit would hang if there is no bail out # condition cnt += 1 if cnt == 50: break # sleep 250ms sleep(0.250) try: # we need to catch the exception if the interface is not up due to # reason stated above VTunIf(openvpn['intf']).set_alias(openvpn['description']) except: pass # TAP interface needs to be brought up explicitly if openvpn['type'] == 'tap': if not openvpn['disable']: VTunIf(openvpn['intf']).set_state('up') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)