#!/usr/bin/env python3 # # 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 # 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 copy import deepcopy from sys import exit,stderr from ipaddress import ip_address,ip_network,IPv4Address,IPv4Network,IPv6Address,IPv6Network,summarize_address_range from netifaces import interfaces from time import sleep from shutil import rmtree from vyos.config import Config from vyos.ifconfig import VTunIf from vyos.template import render from vyos.util import call, chown, chmod_600, chmod_755 from vyos.validate import is_addr_assigned, is_bridge_member from vyos import ConfigError user = 'openvpn' group = 'openvpn' default_config_data = { 'address': [], 'auth_user': '', 'auth_pass': '', 'auth_user_pass_file': '', '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, 'ipv6_local_address': [], 'ipv6_remote_address': [], 'is_bridge_member': False, 'ping_restart': '60', 'ping_interval': '10', 'local_address': [], 'local_address_subnet': '', 'local_host': '', 'local_port': '', 'mode': '', 'ncp_ciphers': '', 'options': [], 'persistent_tunnel': False, 'protocol': 'udp', 'redirect_gateway': '', 'remote_address': [], 'remote_host': [], 'remote_port': '', 'client': [], 'server_domain': '', 'server_max_conn': '', 'server_dns_nameserver': [], 'server_pool': True, 'server_pool_start': '', 'server_pool_stop': '', 'server_pool_netmask': '', 'server_push_route': [], 'server_reject_unconfigured': False, 'server_subnet': [], 'server_topology': '', 'server_ipv6_dns_nameserver': [], 'server_ipv6_local': '', 'server_ipv6_prefixlen': '', 'server_ipv6_remote': '', 'server_ipv6_pool': True, 'server_ipv6_pool_base': '', 'server_ipv6_pool_prefixlen': '', 'server_ipv6_push_route': [], 'server_ipv6_subnet': [], '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 get_config_name(intf): cfg_file = f'/run/openvpn/{intf}.conf' return cfg_file 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 getDefaultServer(network, topology, devtype): """ Gets the default server parameters for a IPv4 "server" directive. Logic from openvpn's src/openvpn/helper.c. Returns a dict with addresses or False if the input parameters were incorrect. """ if not (devtype == 'tun' or devtype == 'tap'): return False if not network.version == 4: return False elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30): return False server = { 'local': '', 'remote_netmask': '', 'client_remote_netmask': '', 'pool_start': '', 'pool_stop': '', 'pool_netmask': '' } if devtype == 'tun': if topology == 'net30' or topology == 'point-to-point': server['local'] = network[1] server['remote_netmask'] = network[2] server['client_remote_netmask'] = server['local'] # pool start is 4th host IP in subnet (.4 in a /24) server['pool_start'] = network[4] if network.prefixlen == 29: server['pool_stop'] = network.broadcast_address else: # pool end is -4 from the broadcast address (.251 in a /24) server['pool_stop'] = network[-5] elif topology == 'subnet': server['local'] = network[1] server['remote_netmask'] = str(network.netmask) server['client_remote_netmask'] = server['remote_netmask'] server['pool_start'] = network[2] server['pool_stop'] = network[-3] server['pool_netmask'] = server['remote_netmask'] elif devtype == 'tap': server['local'] = network[1] server['remote_netmask'] = str(network.netmask) server['client_remote_netmask'] = server['remote_netmask'] server['pool_start'] = network[2] server['pool_stop'] = network[-2] server['pool_netmask'] = server['remote_netmask'] return server 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 # check if interface is member if a bridge openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf']) 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) # bridged server should not have a pool by default (but can be specified manually) if openvpn['bridge_member']: openvpn['server_pool'] = False openvpn['server_ipv6_pool'] = False # 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'): for tmp in conf.list_nodes('local-address'): tmp_ip = ip_address(tmp) if tmp_ip.version == 4: openvpn['local_address'].append(tmp) if conf.exists('local-address {} subnet-mask'.format(tmp)): openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(tmp)) elif tmp_ip.version == 6: # input IPv6 address could be expanded so get the compressed version openvpn['ipv6_local_address'].append(str(tmp_ip)) # 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'): openvpn['mode'] = conf.return_value('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'): for tmp in conf.return_values('remote-address'): tmp_ip = ip_address(tmp) if tmp_ip.version == 4: openvpn['remote_address'].append(tmp) elif tmp_ip.version == 6: openvpn['ipv6_remote_address'].append(str(tmp_ip)) # 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) server_network_v4 = None server_network_v6 = None if conf.exists('server subnet'): for tmp in conf.return_values('server subnet'): tmp_ip = ip_network(tmp) if tmp_ip.version == 4: server_network_v4 = tmp_ip # convert the network to format: "192.0.2.0 255.255.255.0" for later use in template openvpn['server_subnet'].append(tmp_ip.with_netmask.replace(r'/', ' ')) elif tmp_ip.version == 6: server_network_v6 = tmp_ip openvpn['server_ipv6_subnet'].append(str(tmp_ip)) # 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': [], 'ipv6_ip': [], 'ipv6_remote': '', 'ipv6_push_route': [], 'ipv6_subnet': [], 'push_route': [], 'subnet': [], 'remote_netmask': '' } # Option to disable client connection if conf.exists('disable'): data['disable'] = True # IP address of the client for tmp in conf.return_values('ip'): tmp_ip = ip_address(tmp) if tmp_ip.version == 4: data['ip'].append(tmp) elif tmp_ip.version == 6: data['ipv6_ip'].append(str(tmp_ip)) # Route to be pushed to the client for tmp in conf.return_values('push-route'): tmp_ip = ip_network(tmp) if tmp_ip.version == 4: data['push_route'].append(tmp_ip.with_netmask.replace(r'/', ' ')) elif tmp_ip.version == 6: data['ipv6_push_route'].append(str(tmp_ip)) # Subnet belonging to the client for tmp in conf.return_values('subnet'): tmp_ip = ip_network(tmp) if tmp_ip.version == 4: data['subnet'].append(tmp_ip.with_netmask.replace(r'/', ' ')) elif tmp_ip.version == 6: data['ipv6_subnet'].append(str(tmp_ip)) # Append to global client list openvpn['client'].append(data) # re-set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf']) # Server client IP pool if conf.exists('server client-ip-pool'): conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ip-pool') # enable or disable server_pool where necessary # default is enabled, or disabled in bridge mode openvpn['server_pool'] = not conf.exists('disable') if conf.exists('start'): openvpn['server_pool_start'] = conf.return_value('start') if conf.exists('stop'): openvpn['server_pool_stop'] = conf.return_value('stop') if conf.exists('netmask'): openvpn['server_pool_netmask'] = conf.return_value('netmask') conf.set_level('interfaces openvpn ' + openvpn['intf']) # Server client IPv6 pool if conf.exists('server client-ipv6-pool'): conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ipv6-pool') openvpn['server_ipv6_pool'] = not conf.exists('disable') if conf.exists('base'): tmp = conf.return_value('base').split('/') openvpn['server_ipv6_pool_base'] = str(IPv6Address(tmp[0])) if 1 < len(tmp): openvpn['server_ipv6_pool_prefixlen'] = tmp[1] 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'): for tmp in conf.return_values('server name-server'): tmp_ip = ip_address(tmp) if tmp_ip.version == 4: openvpn['server_dns_nameserver'].append(tmp) elif tmp_ip.version == 6: openvpn['server_ipv6_dns_nameserver'].append(str(tmp_ip)) # Route to be pushed to all clients if conf.exists('server push-route'): for tmp in conf.return_values('server push-route'): tmp_ip = ip_network(tmp) if tmp_ip.version == 4: openvpn['server_push_route'].append(tmp_ip.with_netmask.replace(r'/', ' ')) elif tmp_ip.version == 6: openvpn['server_ipv6_push_route'].append(str(tmp_ip)) # 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') openvpn['tls'] = True 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' # set default server topology to net30 if openvpn['mode'] == 'server' and not openvpn['server_topology']: openvpn['server_topology'] = 'net30' # Set defaults where necessary. # If any of the input parameters are wrong, # this will return False and no defaults will be set. if server_network_v4 and openvpn['server_topology'] and openvpn['type']: default_server = None default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type']) if default_server: # server-bridge doesn't require a pool so don't set defaults for it if openvpn['server_pool'] and not openvpn['bridge_member']: if not openvpn['server_pool_start']: openvpn['server_pool_start'] = default_server['pool_start'] if not openvpn['server_pool_stop']: openvpn['server_pool_stop'] = default_server['pool_stop'] if not openvpn['server_pool_netmask']: openvpn['server_pool_netmask'] = default_server['pool_netmask'] for client in openvpn['client']: client['remote_netmask'] = default_server['client_remote_netmask'] if server_network_v6: if not openvpn['server_ipv6_local']: openvpn['server_ipv6_local'] = server_network_v6[1] if not openvpn['server_ipv6_prefixlen']: openvpn['server_ipv6_prefixlen'] = server_network_v6.prefixlen if not openvpn['server_ipv6_remote']: openvpn['server_ipv6_remote'] = server_network_v6[2] if openvpn['server_ipv6_pool'] and server_network_v6.prefixlen < 112: if not openvpn['server_ipv6_pool_base']: openvpn['server_ipv6_pool_base'] = server_network_v6[0x1000] if not openvpn['server_ipv6_pool_prefixlen']: openvpn['server_ipv6_pool_prefixlen'] = openvpn['server_ipv6_prefixlen'] for client in openvpn['client']: client['ipv6_remote'] = openvpn['server_ipv6_local'] if openvpn['redirect_gateway']: openvpn['redirect_gateway'] += ' ipv6' return openvpn def verify(openvpn): if openvpn['deleted']: if openvpn['is_bridge_member']: interface = openvpn['intf'] bridge = openvpn['is_bridge_member'] raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') 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 openvpn['ncp_ciphers']: raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']: if not (openvpn['local_address'] or openvpn['ipv6_local_address']): raise ConfigError('Must specify "local-address" or "bridge member interface"') if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1: raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"') if len(openvpn['remote_address']) > 1 or len(openvpn['ipv6_remote_address']) > 1: raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "remote-address"') for host in openvpn['remote_host']: if host in openvpn['remote_address'] or host in openvpn['ipv6_remote_address']: raise ConfigError('"remote-address" cannot be the same as "remote-host"') if openvpn['local_address'] and not (openvpn['remote_address'] or openvpn['local_address_subnet']): raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"') if openvpn['remote_address'] and not openvpn['local_address']: raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"') if openvpn['ipv6_local_address'] and not openvpn['ipv6_remote_address']: raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"') if openvpn['ipv6_remote_address'] and not openvpn['ipv6_local_address']: raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"') if openvpn['type'] == 'tun': if not (openvpn['remote_address'] or openvpn['ipv6_remote_address']): raise ConfigError('Must specify "remote-address"') if ( (openvpn['local_address'] and openvpn['local_address'] == openvpn['remote_address']) or (openvpn['ipv6_local_address'] and openvpn['ipv6_local_address'] == openvpn['ipv6_remote_address']) ): raise ConfigError('"local-address" and "remote-address" cannot be the same') if openvpn['local_host'] in openvpn['local_address'] or openvpn['local_host'] in openvpn['ipv6_local_address']: raise ConfigError('"local-address" cannot be the same as "local-host"') else: # checks for client-server or site-to-site bridged if openvpn['local_address'] or openvpn['ipv6_local_address'] or openvpn['remote_address'] or openvpn['ipv6_remote_address']: raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or 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 len(openvpn['server_subnet']) > 1 or len(openvpn['server_ipv6_subnet']) > 1: raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 server subnet') for client in openvpn['client']: if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1: raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP') if openvpn['server_subnet']: subnet = IPv4Network(openvpn['server_subnet'][0].replace(' ', '/')) if openvpn['type'] == 'tun' and subnet.prefixlen > 29: raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') elif openvpn['type'] == 'tap' and subnet.prefixlen > 30: raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') for client in openvpn['client']: if client['ip'] and not IPv4Address(client['ip'][0]) in subnet: raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}') else: if not openvpn['bridge_member']: raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode') if openvpn['server_pool']: if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']): raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode') else: v4PoolStart = IPv4Address(openvpn['server_pool_start']) v4PoolStop = IPv4Address(openvpn['server_pool_stop']) if v4PoolStart > v4PoolStop: raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') v4PoolSize = int(v4PoolStop) - int(v4PoolStart) if v4PoolSize >= 65536: raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.') v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) for client in openvpn['client']: if client['ip']: for v4PoolNet in v4PoolNets: if IPv4Address(client['ip'][0]) in v4PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.', file=stderr) if openvpn['server_ipv6_subnet']: if not openvpn['server_subnet']: raise ConfigError('IPv6 server requires an IPv4 server subnet') if openvpn['server_ipv6_pool']: if not openvpn['server_pool']: raise ConfigError('IPv6 server pool requires an IPv4 server pool') if int(openvpn['server_ipv6_pool_prefixlen']) >= 112: raise ConfigError('IPv6 server pool must be larger than /112') v6PoolStart = IPv6Address(openvpn['server_ipv6_pool_base']) v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536 if v6PoolSize < v4PoolSize: raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})') v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop)) for client in openvpn['client']: if client['ipv6_ip']: for v6PoolNet in v6PoolNets: if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.', file=stderr) else: if openvpn['server_ipv6_push_route']: raise ConfigError('IPv6 push-route requires an IPv6 server subnet') for client in openvpn ['client']: if client['ipv6_ip']: raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet') if client['ipv6_push_route']: raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"') if client['ipv6_subnet']: raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"') 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') 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 in advance which clients have been, # remove all client configs ccd_dir = os.path.join(directory, 'ccd', interface) if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) # create config directory on demand directories = [] directories.append(f'{directory}/status') directories.append(f'{directory}/ccd/{interface}') for onedir in directories: if not os.path.exists(onedir): os.makedirs(onedir, 0o755) chown(onedir, user, group) # Fix file permissons for keys fix_permissions = [] fix_permissions.append(openvpn['shared_secret_file']) fix_permissions.append(openvpn['tls_key']) # Generate User/Password authentication file openvpn['auth_user_pass_file'] = f'/run/openvpn/{interface}.pw' if openvpn['auth']: with open(openvpn['auth_user_pass_file'], 'w') as f: f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) # also change permission on auth file fix_permissions.append(openvpn['auth_user_pass_file']) else: # delete old auth file if present if os.path.isfile(openvpn['auth_user_pass_file']): os.remove(openvpn['auth_user_pass_file']) # Generate client specific configuration for client in openvpn['client']: client_file = os.path.join(ccd_dir, client['name']) render(client_file, 'openvpn/client.conf.tmpl', client) chown(client_file, user, group) # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"')) chown(get_config_name(interface), user, group) # Fixup file permissions for file in fix_permissions: chmod_600(file) return None def apply(openvpn): interface = openvpn['intf'] call(f'systemctl stop openvpn@{interface}.service') # 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(interface)): os.remove(get_config_name(interface)) # cleanup client config dir directory = os.path.dirname(get_config_name(interface)) ccd_dir = os.path.join(directory, 'ccd', interface) if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) 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 interface in interfaces(): sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process call(f'systemctl start openvpn@{interface}.service') # better late then sorry ... but we can only set interface alias after # OpenVPN has been launched and created the interface cnt = 0 while interface 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(interface) # 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(interface).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)