From 49153d4e138c762d00db471febb9fd312c0ab122 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 1 Aug 2019 12:31:44 +0200 Subject: openvpn: T1548: initial rewrite with XML and Python --- debian/control | 3 + interface-definitions/interfaces-openvpn.xml | 624 ++++++++++++++++++ src/conf_mode/interface-openvpn.py | 903 +++++++++++++++++++++++++++ 3 files changed, 1530 insertions(+) create mode 100644 interface-definitions/interfaces-openvpn.xml create mode 100755 src/conf_mode/interface-openvpn.py diff --git a/debian/control b/debian/control index c8946e991..a65d0158e 100644 --- a/debian/control +++ b/debian/control @@ -58,6 +58,9 @@ Depends: python3, pdns-recursor, lcdproc, lcdproc-extra-drivers, + openvpn, + openvpn-auth-ldap, + openvpn-auth-radius, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml new file mode 100644 index 000000000..f2eb1ebab --- /dev/null +++ b/interface-definitions/interfaces-openvpn.xml @@ -0,0 +1,624 @@ + + + + + + + OpenVPN tunnel interface name + 460 + + ^vtun[0-9]+$ + + OpenVPN tunnel interface must be named vtunN + + vtunN + OpenVPN interface name + + + + + + Authentication options + + + + + OpenVPN password used for authentication + + + + + OpenVPN username used for authentication + + + + + + + Interface to be added to a bridge group + + + + + Interface to a bridge-group + + + + + + + + Path cost for this port + + 0-2147483647 + Path cost for this port + + + + + + + + + Path priority for this port + + 0-255 + Path priority for this port + + + + + + + + + + + Description + + + + + OpenVPN interface device-type + + tun tap + + + tun + TUN device, required for OSI layer 3 + + + tap + TAP device, required for OSI layer 2 + + + (tun|tap) + + + + + + Disable interface + + + + + Data Encryption Algorithm + + des 3des bf128 bf256 aes128 aes192 aes256 + + + des + DES algorithm + + + 3des + DES algorithm with triple encryption + + + bf128 + Blowfish algorithm with 128-bit key + + + bf256 + Blowfish algorithm with 256-bit key + + + aes128 + AES algorithm with 128-bit key + + + aes192 + AES algorithm with 192-bit key + + + aes256 + AES algorithm with 256-bit key + + + (des|3des|bf128|bf256|aes128|aes192|aes256) + + + + + + Hashing Algorithm + + md5 sha1 sha256 sha384 sha512 + + + md5 + MD5 algorithm + + + sha1 + SHA-1 algorithm + + + sha256 + SHA-256 algorithm + + + sha384 + SHA-384 algorithm + + + sha512 + SHA-512 algorithm + + + (md5|sha1|sha256|sha384|sha512) + + + + + + Keepalive helper options + + + + + Maximum number of keepalive packet failures [default 6] + + 0-1000 + Maximum number of keepalive packet failures + + + + + + + + + Keepalive packet interval (seconds) [default 10] + + 0-600 + Keepalive packet interval (seconds) + + + + + + + + + + + Local IP address of tunnel + + + + + + + + Subnet-mask for local IP address of tunnel + + + + + + + + + + Local IP address to accept connections (all if not set) + + ipv4 + Local IPv4 address + + + + + + + + + Local port number to accept connections + + 1-65535 + Numeric IP port + + + + + + + + + OpenVPN mode of operation + + site-to-site client server + + + site-to-site + Site-to-site mode + + + client + Client in client-server mode + + + server + Server in client-server mode + + + (site-to-site|client|server) + + + + + + Additional OpenVPN options. You must + use the syntax of openvpn.conf in this text-field. Using this + without proper knowledge may result in a crashed OpenVPN server. + Check system log to look for errors. + + + + + + Do not close and reopen interface (TUN/TAP device) on client restarts + + + + + + OpenVPN communication protocol + + udp tcp-passive tcp-active + + + udp + Site-to-site mode + + + tcp-passive + TCP and accepts connections passively + + + tcp-active + TCP and initiates connections actively + + + (udp|tcp-passive|tcp-active) + + + + + + IP address of remote end of tunnel + + ipv4 + Remote end IPv4 address + + + + + + + + + Remote host to connect to (dynamic if not set) + + ipv4 + IP address of remote host + + + txt + Hostname of remote host + + + + + + + Remote port number to connect to + + 1-65535 + Numeric IP port + + + + + + + + + OpenVPN tunnel to be used as the default route + + + + + Tunnel endpoints are on the same subnet + + + + + + + Server-mode options + + + + + Two Factor Authentication providers + + + + + Authy Two Factor Authentication providers + + + + + Authy api key + + + + + Authy users (must be email address) + + [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$ + + Invalid email address + + + + + Country calling codes + + [0-9]+$ + + Invalid Country Calling Code + + + + + Mobile phone number + + [0-9]+$ + + Invalid Phone Number + + + + + + + + + + + Client-specific settings + + name + Client common-name in the certificate + + + + + + Option to disable client connection + + + + + + IP address of the client + + ipv4 + Client IPv4 address + + + + + + + + + Route to be pushed to the client + + ipv4net + IPv4 network and prefix length + + + + + + + + + + Subnet belonging to the client + + ipv4net + IPv4 network and prefix length belonging to the client + + + + + + + + + + + + DNS suffix to be pushed to all clients + + txt + Domain Name Server suffix + + + + + + Number of maximum client connections + + 1-4096 + Number of concurrent clients + + + + + + + + + Domain Name Server (DNS) + + ipv4 + DNS server IPv4 address + + + + + + + + + + Route to be pushed to all clients + + ipv4net + IPv4 network and prefix length + + + + + + + + + + Reject connections from clients that are not explicitly configured + + + + + Server-mode subnet (from which client IPs are allocated) + + ipv4net + IPv4 address and prefix length + + + + + + + + + Topology for clients + + point-to-point subnet + + + point-to-point + Point-to-point topology + + + subnet + Subnet topology + + + (subnet|point-to-point) + + + + + + + + File containing the secret key shared with remote end of tunnel + + file + File in /config/auth directory + + + + + + + + + Transport Layer Security (TLS) options + + + + + File containing certificate for Certificate Authority (CA) + + + + + File containing certificate for this host + + + + + File containing certificate revocation list (CRL) for this host + + + + + File containing Diffie Hellman parameters (server only) + + + + + File containing this host's private key + + + + + File containing this host's private key + + active passive + + + active + Initiate TLS negotiation actively + + + passive + Waiting for TLS connections passively + + + (active|passive) + + + + + + + + Use fast LZO compression on this TUN/TAP interface + + + + + + + + diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py new file mode 100755 index 000000000..1420cabe9 --- /dev/null +++ b/src/conf_mode/interface-openvpn.py @@ -0,0 +1,903 @@ +#!/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 +import pwd +import grp +import sys +import stat +import copy +import jinja2 +import psutil +from ipaddress import ip_address,ip_network,IPv4Interface + +from signal import SIGUSR1 +from subprocess import Popen, PIPE + +from vyos.config import Config +from vyos import ConfigError +from vyos.validate import is_addr_assigned + +# 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 + +verb 3 +status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 +writepid /var/run/openvpn/{{ intf }}.pid +daemon openvpn-{{ intf }} + +dev-type {{ type }} +dev {{ intf }} +user {{ uid }} +group {{ gid }} +persist-key + +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 'site-to-site' in server_topology %}p2p{% else %}{{ server_topology }}{% 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 'tap' in type %} +ifconfig {{ local_address }} {{ local_address_subnet }} +{% else %} +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_crl %} +crl-verify {{ tls_crl }} +{% endif %} + +{%- if tls_dh %} +dh {{ tls_dh }} +{% 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 'aes128' in encryption %} +cipher aes-128-cbc +{%- elif 'aes192' in encryption %} +cipher aes-192-cbc +{%- elif 'aes256' in encryption %} +cipher aes-256-cbc +{% endif %} +{% 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 %} + +{% for option in options -%} +{{ option }} +{% endfor -%} + +{%- if server_2fa_authy_key %} +plugin /usr/lib/authy/authy-openvpn.so https://api.authy.com/protected/json {{ server_2fa_authy_key }} nopam +{% endif %} +""" + +client_tmpl = """ +### Autogenerated by interfaces-openvpn.py ### + +ifconfig-push {{ ip }} {{ remote_netmask }} +{% for route in push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for net in subnet -%} +iroute {{ net }} +{% endfor -%} + +{%- if disabled %} +disable +{% endif %} +""" + +default_config_data = { + 'address': [], + 'auth_user': '', + 'auth_pass': '', + 'auth': False, + 'bridge_member': [], + 'compress_lzo': False, + 'deleted': False, + 'description': '', + 'disabled': False, + 'encryption': '', + 'hash': '', + 'intf': '', + 'ping_restart': '60', + 'ping_interval': '10', + 'local_address': '', + 'local_address_subnet': '', + 'local_host': '', + 'local_port': '', + 'mode': '', + 'options': [], + 'persistent_tunnel': False, + 'protocol': '', + 'redirect_gateway': '', + 'remote_address': '', + 'remote_host': [], + 'remote_port': '', + 'server_2fa_authy_key': '', + 'server_2fa_authy': [], + '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_ca_cert': '', + 'tls_cert': '', + 'tls_crl': '', + 'tls_dh': '', + 'tls_key': '', + 'tls_role': '', + 'type': 'tun', + 'uid': 'nobody', + 'gid': 'nogroup', +} + +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 fixup_permission(filename, permission=stat.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 = pwd.getpwnam('root').pw_uid + gid = grp.getgrnam('vyattacfg').gr_gid + os.chown(filename, uid, gid) + +def checkCertHeader(header, filename): + """ + Verify if filename contains specified header. + Returns True on success or on file not found to not trigger the exceptions + """ + if not os.path.isfile(filename): + return True + + with open(filename, 'r') as f: + for line in f: + if re.match(header, line): + return True + + return False + +def get_config(): + openvpn = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + print('Executing for interface ' + openvpn['intf']) + + # 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') + + # interface disabled + if conf.exists('disabled'): + openvpn['disabled'] = True + + # data encryption algorithm + if conf.exists('encryption'): + openvpn['encryption'] = conf.return_value('encryption') + + # hash algorithm + if conf.exists('hash'): + openvpn['hash'] = conf.return_value('hash') + + # Maximum number of keepalive packet failures + if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'): + fail_count = conf.return_value('keep-alive failure-count') + interval = conf.return_value('keep-alive interval') + openvpn['ping_interval' ] = interval + openvpn['ping_restart' ] = int(interval) * int(fail_count) + + # Local IP address of tunnel - even as it is a tag node - we can only work + # on the first address + if conf.exists('local-address'): + openvpn['local_address'] = conf.list_nodes('local-address')[0] + if conf.exists('local-address {} subnet-mask'.format(openvpn['local_address'])): + openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(openvpn['local_address'])) + + # Local IP address to accept connections + if conf.exists('local-host'): + openvpn['local_host'] = conf.return_value('local-host') + + # Local port number to accept connections + if conf.exists('local-port'): + openvpn['local_port'] = conf.return_value('local-port') + + # OpenVPN operation mode + if conf.exists('mode'): + mode = conf.return_value('mode') + openvpn['mode'] = mode + + # Additional OpenVPN options + if conf.exists('openvpn-option'): + openvpn['options'] = conf.return_values('openvpn-option') + + # Do not close and reopen interface + if conf.exists('persistent-tunnel'): + openvpn['persistent_tunnel'] = True + + # Communication protocol + if conf.exists('protocol'): + openvpn['protocol'] = conf.return_value('protocol') + + # IP address of remote end of tunnel + if conf.exists('remote-address'): + openvpn['remote_address'] = conf.return_value('remote-address') + + # Remote host to connect to (dynamic if not set) + if conf.exists('remote-host'): + openvpn['remote_host'] = conf.return_values('remote-host') + + # Remote port number to connect to + if conf.exists('remote-port'): + openvpn['remote_port'] = conf.return_value('remote-port') + + # OpenVPN tunnel to be used as the default route + # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ + # redirect-gateway flags + if conf.exists('replace-default-route'): + openvpn['redirect_gateway'] = 'def1' + + if conf.exists('replace-default-route local'): + openvpn['redirect_gateway'] = 'local def1' + + # Two Factor Authentication providers + # currently limited to authy + if conf.exists('2-factor-authentication authy api-key'): + openvpn['server_2fa_authy_key'] = conf.return_value('2-factor-authentication authy api-key') + + # Authy users (must be email address) + for user in conf.list_nodes('server 2-factor-authentication authy user'): + # set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' 2-factor-authentication authy user ' + user) + data = { + 'user': user, + 'country_code': '', + 'mobile_number': '' + } + + # Country calling codes + if conf.exists('country-calling-code'): + data['country_code'] = conf.return_value('country-calling-code') + + # Mobile phone number + if conf.exists('phone-number'): + data['mobile_number'] = conf.return_value('phone-number') + + openvpn['server_2fa_authy'].append(data) + + # 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, + 'disabled': 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'][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['disabled'] = 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'): + network = conf.return_value('server push-route') + tmp = IPv4Interface(network).with_netmask + openvpn['server_push_route'] = 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 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 + + # Role in TLS negotiation + if conf.exists('tls role'): + openvpn['tls_role'] = conf.return_value('tls role') + 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 + + 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)) + + # + # 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']: + 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"') + + if not openvpn['remote_address']: + raise ConfigError('Must specify "remote-address"') + + if openvpn['local_address'] == openvpn['local_host']: + raise ConfigError('"local-address" cannot be the same as "local-host"') + + for host in openvpn['remote_host']: + if host == openvpn['remote_address']: + raise ConfigError('"remote-address" cannot be the same as "remote-host"') + + if openvpn['local_address'] == openvpn['remote_address']: + raise ConfigError('"local-address" and "remote-address" cannot be the same') + + if openvpn['type'] == 'tap' and openvpn['local_address_subnet'] == '': + raise ConfigError('Must specify "subnet-mask" for local-address') + + 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']: + raise ConfigError('Must specify "tls dh-file" 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 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 not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): + raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) + + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']): + raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) + + if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']): + raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) + + if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): + raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) + + 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']: + 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']: + 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"') + + # + # Auth user/pass + # + if openvpn['auth']: + if not openvpn['auth_user']: + raise ConfigError('Username for authentication is missing') + + if not openvpn['auth_pass']: + raise ConfigError('Password for authentication is missing') + + # + # Client + # + subnet = openvpn['server_subnet'].replace(' ', '/') + for client in openvpn['client']: + if not ip_address(client['ip']) in ip_network(subnet): + raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) + + + + return None + +def generate(openvpn): + if openvpn['deleted']: + return None + + interface = openvpn['intf'] + # create config directory on demand + directory = os.path.dirname(get_config_name(interface)) + if not os.path.exists(directory): + os.mkdir(directory) + + # create status directory on demand + if not os.path.exists(directory + '/status'): + os.mkdir(directory + '/status') + + # fix permission on status directory + os.chmod(directory + '/status', stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) + uid = pwd.getpwnam(openvpn['uid']).pw_uid + gid = grp.getgrnam(openvpn['gid']).gr_gid + os.chown(directory + '/status', uid, gid) + + # create client config dir on demand + if not os.path.exists(directory + '/ccd/'): + os.mkdir(directory + '/ccd/') + + # crete client config dir per interface on demand + if not os.path.exists(directory + '/ccd/' + interface): + os.mkdir(directory + '/ccd/' + interface) + + os.chmod(directory + '/ccd/' + interface, stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) + os.chown(directory + '/ccd/' + interface, uid, gid) + + # 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) + + # Generate client specific configuration + for client in openvpn['client']: + client_file = directory + '/ccd/' + interface + '/' + client['name'] + tmpl = jinja2.Template(client_tmpl) + client_text = tmpl.render(client) + with open(client_file, 'w') as f: + f.write(client_text) + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(openvpn) + with open(get_config_name(interface), 'w') as f: + f.write(config_text) + + return None + + +def apply(openvpn): + interface = openvpn['intf'] + + pid = 0 + pidfile = '/var/run/openvpn/{}.pid'.format(interface) + if os.path.isfile(pidfile): + pid = 0 + with open(pidfile, 'r') as f: + pid = int(f.read()) + + # If tunnel interface has been deleted - stop service + if openvpn['deleted']: + directory = os.path.dirname(get_config_name(interface)) + + # we only need to stop the demon if it's running + # daemon could have died or killed by someone + if psutil.pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) + + # cleanup old PID file + if os.path.isfile(pidfile): + os.remove(pidfile) + + # cleanup old configuration file + if os.path.isfile(get_config_name(interface)): + os.remove(get_config_name(interface)) + + # cleanup client config dir + if os.path.isdir(directory + '/ccd/' + interface): + os.remove(directory + '/ccd/' + interface + '/*') + + return None + + # Send SIGUSR1 to the process instead of creating a new process + if psutil.pid_exists(pid): + os.kill(pid, SIGUSR1) + return None + + # No matching OpenVPN process running - maybe it got killed or none + # existed - nevertheless, spawn new OpenVPN process + cmd = 'start-stop-daemon --start --quiet' + cmd += ' --pidfile ' + pidfile + cmd += ' --exec /usr/sbin/openvpn' + # now pass arguments to openvpn binary + cmd += ' --' + cmd += ' --config ' + get_config_name(interface) + + # execute assembled command + subprocess_cmd(cmd) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From fdb474235a8ce7fd0d5cc9fd74e5c880eb2093e6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 00:02:11 +0200 Subject: openvpn: T1548: add op-mode command for key generation --- op-mode-definitions/openvpn.xml | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 op-mode-definitions/openvpn.xml diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml new file mode 100644 index 000000000..44f8e01e9 --- /dev/null +++ b/op-mode-definitions/openvpn.xml @@ -0,0 +1,48 @@ + + + + + + + OpenVPN key generation tool + + + + + Generate shared-secret key with specified file name + + <filename> + + + + result=1; + key_path=$4 + full_path= + + # Prepend /config/auth if the path is not absolute + if echo $key_path | egrep -ve '^/.*' > /dev/null; then + full_path=/config/auth/$key_path + else + full_path=$key_path + fi + + key_dir=`dirname $full_path` + if [ ! -d $key_dir ]; then + echo "Directory $key_dir does not exist!" + exit 1 + fi + + echo "Generating OpenVPN key to $full_path" + sudo /usr/sbin/openvpn --genkey --secret "$full_path" + result=$? + if [ $result = 0 ]; then + echo "Your new local OpenVPN key has been generated" + fi + /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path" + + + + + + + -- cgit v1.2.3 From 1fd513bb0ada9b892a790c2fd26537a19976a589 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 00:58:20 +0200 Subject: openvpn: T1548: fix file ownership of client configuration file --- src/conf_mode/interface-openvpn.py | 54 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 1420cabe9..d63d63acf 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -34,6 +34,9 @@ from vyos.config import Config from vyos import ConfigError from vyos.validate import is_addr_assigned +user = 'nobody' +group = 'nogroup' + # Please be careful if you edit the template. config_tmpl = """ ### Autogenerated by interfaces-openvpn.py ### @@ -281,8 +284,8 @@ default_config_data = { 'tls_key': '', 'tls_role': '', 'type': 'tun', - 'uid': 'nobody', - 'gid': 'nogroup', + 'uid': user, + 'gid': group, } def subprocess_cmd(command): @@ -293,6 +296,17 @@ 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 + os.chmod(directory, stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + os.chown(directory, uid, gid) + def fixup_permission(filename, permission=stat.S_IRUSR): """ Check if the given file exists and change ownershit to root/vyattacfg @@ -784,31 +798,16 @@ def generate(openvpn): return None interface = openvpn['intf'] - # create config directory on demand directory = os.path.dirname(get_config_name(interface)) - if not os.path.exists(directory): - os.mkdir(directory) + # create config directory on demand + openvpn_mkdir(directory) # create status directory on demand - if not os.path.exists(directory + '/status'): - os.mkdir(directory + '/status') - - # fix permission on status directory - os.chmod(directory + '/status', stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) - uid = pwd.getpwnam(openvpn['uid']).pw_uid - gid = grp.getgrnam(openvpn['gid']).gr_gid - os.chown(directory + '/status', uid, gid) - + openvpn_mkdir(directory + '/status') # create client config dir on demand - if not os.path.exists(directory + '/ccd/'): - os.mkdir(directory + '/ccd/') - + openvpn_mkdir(directory + '/ccd') # crete client config dir per interface on demand - if not os.path.exists(directory + '/ccd/' + interface): - os.mkdir(directory + '/ccd/' + interface) - - os.chmod(directory + '/ccd/' + interface, stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) - os.chown(directory + '/ccd/' + interface, uid, gid) + openvpn_mkdir(directory + '/ccd/' + interface) # Fix file permissons for keys fixup_permission(openvpn['shared_secret_file']) @@ -822,6 +821,10 @@ def generate(openvpn): fixup_permission(auth_file) + # get numeric uid/gid + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + # Generate client specific configuration for client in openvpn['client']: client_file = directory + '/ccd/' + interface + '/' + client['name'] @@ -829,11 +832,13 @@ def generate(openvpn): client_text = tmpl.render(client) with open(client_file, 'w') as f: f.write(client_text) + os.chown(client_file, uid, gid) tmpl = jinja2.Template(config_tmpl) config_text = tmpl.render(openvpn) with open(get_config_name(interface), 'w') as f: f.write(config_text) + os.chown(get_config_name(interface), uid, gid) return None @@ -869,7 +874,10 @@ def apply(openvpn): # cleanup client config dir if os.path.isdir(directory + '/ccd/' + interface): - os.remove(directory + '/ccd/' + interface + '/*') + try: + os.remove(directory + '/ccd/' + interface + '/*') + except: + pass return None -- cgit v1.2.3 From 1fea0d1cd6232033bde839642446fad162f6f8c8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 02:07:07 +0200 Subject: openvpn: T1548: add op-mode command for resetting client vyos@vyos:~$ run reset openvpn client client1 --- op-mode-definitions/openvpn.xml | 17 ++++++++++ src/completion/list_openvpn_clients.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100755 src/completion/list_openvpn_clients.py diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 44f8e01e9..9c9c3b3ad 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -45,4 +45,21 @@ + + + + + + + Reset specified OpenVPN client + + + + + echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null + + + + + diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py new file mode 100755 index 000000000..828ce6b5e --- /dev/null +++ b/src/completion/list_openvpn_clients.py @@ -0,0 +1,57 @@ +#!/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 sys +import argparse + +from vyos.interfaces import list_interfaces_of_type + +def get_client_from_interface(interface): + clients = [] + with open('/opt/vyatta/etc/openvpn/status/' + interface + '.status', 'r') as f: + dump = False + for line in f: + if line.startswith("Common Name,"): + dump = True + continue + if line.startswith("ROUTING TABLE"): + dump = False + continue + if dump: + # client entry in this file looks like + # client1,172.18.202.10:47495,2957,2851,Sat Aug 17 00:07:11 2019 + # we are only interested in the client name 'client1' + clients.append(line.split(',')[0]) + + return clients + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--interface", type=str, help="List connected clients per interface") + parser.add_argument("-a", "--all", action='store_true', help="List all connected OpenVPN clients") + args = parser.parse_args() + + clients = [] + + if args.interface: + clients = get_client_from_interface(args.interface) + elif args.all: + for interface in list_interfaces_of_type("openvpn"): + clients += get_client_from_interface(interface) + + print(" ".join(clients)) + -- cgit v1.2.3 From dbffd657b46fe0edcba67141bf87173448043b70 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 02:15:51 +0200 Subject: openvpn: T1548: add op-mode command for resetting vyos@vyos:~$ reset openvpn interface vtun10 --- op-mode-definitions/openvpn.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 9c9c3b3ad..4a7f985e9 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -58,6 +58,15 @@ echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null + + + Reset OpenVPN process on interface + + + + + sudo kill -SIGUSR1 $(cat /var/run/openvpn/$4.pid) + -- cgit v1.2.3 From e11d7b58ad89eb50e3de7e1c0516e707baff07a4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 02:19:53 +0200 Subject: openvpn: T1548: remove debug output --- src/conf_mode/interface-openvpn.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index d63d63acf..339668f5f 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -345,8 +345,6 @@ def get_config(): except KeyError as E: print("Interface not specified") - print('Executing for interface ' + openvpn['intf']) - # Check if interface instance has been removed if not conf.exists('interfaces openvpn ' + openvpn['intf']): openvpn['deleted'] = True -- cgit v1.2.3