#!/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_forwarding': True,
'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')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
openvpn['ipv6_forwarding'] = False
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
openvpn['ipv6_dup_addr_detect'] = conf.return_value('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'])
# Disable IPv6 forwarding on this interface
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)