Page MenuHomeVyOS Platform

interfaces-openvpn.py
No OneTemporary

Size
43 KB
Referenced Files
None
Subscribers
None

interfaces-openvpn.py

#!/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 <http://www.gnu.org/licenses/>.
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.configdict import list_diff
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_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
default_config_data = {
'address': [],
'auth_user': '',
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
'compress_lzo': False,
'deleted': False,
'description': '',
'disable': False,
'disable_ncp': False,
'encryption': '',
'hash': '',
'intf': '',
'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'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',
'protocol_real': '',
'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']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
# check if interface is member of a bridge
openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
return openvpn
# bridged server should not have a pool by default (but can be specified manually)
if openvpn['is_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 prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Determine currently effective EUI64 addresses - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('ipv6 address eui64')
openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
# Remove the default link-local address if set.
if conf.exists('ipv6 address no-default-link-local'):
openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
else:
# add the link-local by default to make IPv6 work
openvpn['ipv6_eui64_prefix'].append('fe80::/64')
# 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'))
# to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
# accept_ra must be 2
if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
openvpn['ipv6_accept_ra'] = 2
# 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'
# Convert protocol to real protocol used by openvpn.
# To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols
# (https://community.openvpn.net/openvpn/ticket/360), unless local is IPv4
# in which case it must use the standard protocols.
# Note: this will break openvpn if IPv6 is disabled on the system.
# This currently isn't supported, a check can be added in the future.
if openvpn['protocol'] == 'tcp-active':
openvpn['protocol_real'] = 'tcp6-client'
elif openvpn['protocol'] == 'tcp-passive':
openvpn['protocol_real'] = 'tcp6-server'
else:
openvpn['protocol_real'] = 'udp6'
if is_ipv4(openvpn['local_host']):
# takes out the '6'
openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:]
# 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['is_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']:
raise ConfigError((
f'Cannot delete interface "{openvpn["intf"]}" as it is a '
f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
# 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['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
raise ConfigError('Must specify "local-address" or add interface to bridge')
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['is_bridge_member']:
raise ConfigError('Must specify "server subnet" or add interface to bridge 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):
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
if openvpn['deleted'] or openvpn['disable']:
return None
# 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
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("&quot;", '"'))
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 files
cleanup = []
cleanup.append(get_config_name(interface))
cleanup.append(openvpn['auth_user_pass_file'])
for file in cleanup:
if os.path.isfile(file):
os.unlink(file)
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 accept RA
o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
# IPv6 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
# 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'])
# IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
# If MAC has changed, old EUI64 addresses won't get deleted,
# but this isn't easy to solve, so leave them.
# This is even more difficult as openvpn uses a random MAC for the
# initial interface creation, unless set by 'lladdr'.
# NOTE: right now the interface is always deleted. For future
# compatibility when tap's are not deleted, leave the del_ in
if openvpn['mode'] == 'tap':
for addr in openvpn['ipv6_eui64_prefix_remove']:
o.del_ipv6_eui64_address(addr)
for addr in openvpn['ipv6_eui64_prefix']:
o.add_ipv6_eui64_address(addr)
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)

File Metadata

Mime Type
text/x-script.python
Expires
Sat, Oct 17, 12:20 AM (21 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4349782
Default Alt Text
interfaces-openvpn.py (43 KB)

Event Timeline