#!/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 shutil import rmtree 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 ### {% if ip -%} ifconfig-push {{ ip }} {{ remote_netmask }} {% endif -%} {% 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': '', 'ipv6_autoconf': 0, 'ipv6_eui64_prefix': '', 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, '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') # 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 # 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 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 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 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) 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 # 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(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 # 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 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']: VTunIf(openvpn['intf']).set_admin_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)