From b6ef81d2667cb875130225c7faabdf34e44fc17c Mon Sep 17 00:00:00 2001 From: zsdc Date: Tue, 13 Aug 2019 12:27:36 +0300 Subject: [hostname] T1531: Added hostname alias to 127.0.1.1 (Debian way) This change makes "dnsdomainname" and "hostname -f" operable --- src/conf_mode/host_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 16467c8df..2fad57db6 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -45,7 +45,7 @@ config_file_resolv = '/etc/resolv.conf' config_tmpl_hosts = """ ### Autogenerated by host_name.py ### 127.0.0.1 localhost -127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} +127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback -- cgit v1.2.3 From ae7501bd0aeef87b708eb033907aab470ea764b7 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 13 Aug 2019 15:48:04 -0500 Subject: [service https] T1443: move https and api default data to vyos.defaults --- python/vyos/defaults.py | 12 ++++++++++++ src/conf_mode/http-api.py | 11 ++--------- src/conf_mode/https.py | 13 +++---------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 524b80424..ee307cf15 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -27,3 +27,15 @@ cfg_group = 'vyattacfg' cfg_vintage = 'vyatta' commit_lock = '/opt/vyatta/config/.lock' + +https_data = { + 'listen_address' : [ '127.0.0.1' ] +} + +api_data = { + 'listen_address' : '127.0.0.1', + 'port' : '8080', + 'strict' : 'false', + 'debug' : 'false', + 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] +} diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index c1d596ea3..1f91ac582 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -27,14 +27,6 @@ from vyos import ConfigError config_file = '/etc/vyos/http-api.conf' -default_config_data = { - 'listen_address' : '127.0.0.1', - 'port' : '8080', - 'strict' : 'false', - 'debug' : 'false', - 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] -} - vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] # XXX: this model will need to be extended for tag nodes @@ -43,7 +35,8 @@ dependencies = [ ] def get_config(): - http_api = default_config_data + http_api = vyos.defaults.api_data + conf = Config() if not conf.exists('service https api'): return None diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index e1e81eef1..2495cf7e7 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -21,6 +21,7 @@ import os import jinja2 +import vyos.defaults from vyos.config import Config from vyos import ConfigError @@ -75,16 +76,8 @@ server { } """ -default_config_data = { - 'listen_address' : [ '127.0.0.1' ] -} - -default_api_config_data = { - 'port' : '8080', -} - def get_config(): - https = default_config_data + https = vyos.defaults.https_data conf = Config() if not conf.exists('service https'): return None @@ -96,7 +89,7 @@ def get_config(): https['listen_address'] = addrs[:] if conf.exists('api'): - https['api'] = default_api_config_data + https['api'] = vyos.defaults.api_data if conf.exists('api port'): port = conf.return_value('api port') -- cgit v1.2.3 From e304e91a781f79c1e12bb2a7f806a0015bf039e3 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 14 Aug 2019 12:04:49 -0500 Subject: [service https] T1443: add self-signed TLS certificate --- interface-definitions/https.xml | 24 +++++++ python/vyos/defaults.py | 7 ++ src/conf_mode/https.py | 9 +++ src/conf_mode/vyos_cert.py | 143 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100755 src/conf_mode/vyos_cert.py diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index 828de449c..13d5c43ea 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -27,6 +27,30 @@ + + + TLS certificates + + + + + Use an automatically generated self-signed certificate + + + + + + Lifetime in days; default is 365 + + 1-65535 + Number of days + + + + + + + VyOS HTTP API configuration diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index ee307cf15..3e4c02562 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -39,3 +39,10 @@ api_data = { 'debug' : 'false', 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] } + +vyos_cert_data = { + "conf": "/etc/nginx/snippets/vyos-cert.conf", + "crt": "/etc/ssl/certs/vyos-selfsigned.crt", + "key": "/etc/ssl/private/vyos-selfsign", + "lifetime": "365", +} diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 2495cf7e7..289eacf69 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -46,11 +46,16 @@ server { # listen 443 ssl default_server; listen [::]:443 ssl default_server; + +{% if vyos_cert %} + include {{ vyos_cert.conf }}; +{% else %} # # Self signed certs generated by the ssl-cert package # Don't use them in a production server! # include snippets/snakeoil.conf; +{% endif %} {% for l_addr in listen_address %} server_name {{ l_addr }}; @@ -88,6 +93,10 @@ def get_config(): addrs = conf.return_values('listen-address') https['listen_address'] = addrs[:] + if conf.exists('certificates'): + if conf.exists('certificates system-generated-certificate'): + https['vyos_cert'] = vyos.defaults.vyos_cert_data + if conf.exists('api'): https['api'] = vyos.defaults.api_data diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py new file mode 100755 index 000000000..4a44573ca --- /dev/null +++ b/src/conf_mode/vyos_cert.py @@ -0,0 +1,143 @@ +#!/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 sys +import os +import subprocess +import tempfile +import pathlib +import ssl + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError + +vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] + +# XXX: this model will need to be extended for tag nodes +dependencies = [ + 'https.py', +] + +def status_self_signed(cert_data): +# check existence and expiration date + path = pathlib.Path(cert_data['conf']) + if not path.is_file(): + return False + path = pathlib.Path(cert_data['crt']) + if not path.is_file(): + return False + path = pathlib.Path(cert_data['key']) + if not path.is_file(): + return False + + # check if certificate is 1/2 past lifetime, with openssl -checkend + end_days = int(cert_data['lifetime']) + end_seconds = int(0.5*60*60*24*end_days) + checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}' + ''.format(end=end_seconds, **cert_data)) + try: + subprocess.check_call(checkend_cmd, shell=True) + return True + except subprocess.CalledProcessError as err: + if err.returncode == 1: + return False + else: + print("Called process error: {}.".format(err)) + +def generate_self_signed(cert_data): + san_config = None + + if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0): + san_config = tempfile.NamedTemporaryFile() + with open(san_config.name, 'w') as fd: + fd.write('[req]\n') + fd.write('distinguished_name=req\n') + fd.write('[san]\n') + fd.write('subjectAltName=DNS:vyos\n') + + openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' + '-newkey rsa:4096 -keyout {key} -out {crt} ' + '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' + '-extensions san -config {san_conf}' + ''.format(san_conf=san_config.name, + **cert_data)) + + else: + openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' + '-newkey rsa:4096 -keyout {key} -out {crt} ' + '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' + '-addext "subjectAltName=DNS:vyos"' + ''.format(**cert_data)) + + try: + subprocess.check_call(openssl_req_cmd, shell=True) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + + os.chmod('{key}'.format(**cert_data), 0o400) + + with open('{conf}'.format(**cert_data), 'w') as f: + f.write('ssl_certificate {crt};\n'.format(**cert_data)) + f.write('ssl_certificate_key {key};\n'.format(**cert_data)) + + if san_config: + san_config.close() + +def get_config(): + vyos_cert = vyos.defaults.vyos_cert_data + + conf = Config() + if not conf.exists('service https certificates system-generated-certificate'): + return None + else: + conf.set_level('service https certificates system-generated-certificate') + + if conf.exists('lifetime'): + lifetime = conf.return_value('lifetime') + vyos_cert['lifetime'] = lifetime + + return vyos_cert + +def verify(vyos_cert): + return None + +def generate(vyos_cert): + if vyos_cert is None: + return None + + if not status_self_signed(vyos_cert): + generate_self_signed(vyos_cert) + +def apply(vyos_cert): + for dep in dependencies: + cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + raise ConfigError("{}.".format(err)) + +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 6492541b2ee3f7f246682d27974670bd6fbdacbe Mon Sep 17 00:00:00 2001 From: zsdc Date: Wed, 14 Aug 2019 22:03:42 +0300 Subject: [bfd] T1183: Added validations and fixing bugs in BFD: * added validations for "source address IP" and "bfd peer IP" * added check for configuring multihop together with an interface name * fixed "show protocols bfd peer X" for peers with custom options --- interface-definitions/protocols-bfd.xml | 8 ++++++++ op-mode-definitions/show-protocols-bfd.xml | 6 +++--- src/conf_mode/protocols_bfd.py | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index f2d7d7d2f..62e2c87b9 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -20,6 +20,10 @@ ipv6 BFD peer IPv6 address + + + + @@ -46,6 +50,10 @@ ipv6 Local IPv6 address used to connect to the peer + + + + diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml index 2a94d0497..398a81d1b 100644 --- a/op-mode-definitions/show-protocols-bfd.xml +++ b/op-mode-definitions/show-protocols-bfd.xml @@ -24,16 +24,16 @@ Show Bidirectional Forwarding Detection (BFD) peer status - + - /usr/bin/vtysh -c "show bfd peer $5" + /usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 '($0 ~ peer BFD_PEER) { system("/usr/bin/vtysh -c \"show bfd " $0 "\"") }' Show Bidirectional Forwarding Detection (BFD) peer counters - /usr/bin/vtysh -c "show bfd peer $5 counters" + /usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 '($0 ~ peer BFD_PEER) { system("/usr/bin/vtysh -c \"show bfd " $0 " counters\"") }' diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 98f38035a..9ca194edd 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -176,6 +176,10 @@ def verify(bfd): if peer['multihop'] and peer['echo_mode']: raise ConfigError('Multihop and echo-mode cannot be used together') + # multihop doesn't accept interface names + if peer['multihop'] and peer['src_if']: + raise ConfigError('Multihop and source interface cannot be used together') + # echo interval can be configured only with enabled echo-mode if peer['echo_interval'] != '' and not peer['echo_mode']: raise ConfigError('echo-interval can be configured only with enabled echo-mode') -- cgit v1.2.3 From 3d94de9b56ef2a6030ef5cea8b307098688c949d Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Wed, 14 Aug 2019 01:21:20 +0300 Subject: [op-mode] T1590 xml-style rewrite of 'show system' operations --- Makefile | 1 + op-mode-definitions/show-system-info.xml | 167 +++++++++++++++++++++++++++++++ src/helpers/vyos-sudo.py | 40 ++++++++ src/op_mode/show_ram.sh | 33 ++++++ src/op_mode/show_users.py | 111 ++++++++++++++++++++ 5 files changed, 352 insertions(+) create mode 100644 op-mode-definitions/show-system-info.xml create mode 100755 src/helpers/vyos-sudo.py create mode 100755 src/op_mode/show_ram.sh create mode 100755 src/op_mode/show_users.py diff --git a/Makefile b/Makefile index 89b83d4f4..ee01e5ad3 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/monitor/node.def rm -f $(OP_TMPL_DIR)/generate/node.def rm -f $(OP_TMPL_DIR)/show/vpn/node.def + rm -f $(OP_TMPL_DIR)/show/system/node.def .PHONY: all all: clean interface_definitions op_mode_definitions diff --git a/op-mode-definitions/show-system-info.xml b/op-mode-definitions/show-system-info.xml new file mode 100644 index 000000000..ade3829f2 --- /dev/null +++ b/op-mode-definitions/show-system-info.xml @@ -0,0 +1,167 @@ + + + + + + + Show system information + + + + + + Show active network connections on the system + + netstat -an + + + + Show TCP connection information + + ss -t -r + + + + Show all TCP connections + + ss -t -a + + + + Show TCP connection without resolving names + + ss -t -n + + + + + + Show UDP socket information + + ss -u -a -r + + + + Show UDP socket information without resolving names + + ss -u -a -n + + + + + + + + + Show messages in kernel ring buffer + + sudo dmesg + + + + + Show user accounts + + + + + Show user account information + + ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py + + + + Show information about all accounts + + ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py all + + + + Show information about locked accounts + + ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py locked + + + + Show information about non VyOS user accounts + + ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py other + + + + Show information about VyOS user accounts + + ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py vyos + + + + + + + + + Show system memory usage + + ${vyos_op_scripts_dir}/show_ram.sh + + + + Show kernel cache information + + sudo slabtop -o + + + + Show detailed system memory usage + + cat /proc/meminfo + + + + + + + Show system processes + + ps ax + + + + Show extensive process info + + top -b -n1 + + + + Show summary of system processes + + uptime + + + + Show process tree + + ps -ejH + + + + + + + Show filesystem usage + + df -h -x squashfs + + + + + Show how long the system has been up + + uptime + + + + + + + diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py new file mode 100755 index 000000000..0101a0c95 --- /dev/null +++ b/src/helpers/vyos-sudo.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import getpass +import grp +import os +import sys + + +def is_admin() -> bool: + """Look if current user is in sudo group""" + current_user = getpass.getuser() + (_, _, _, admin_group_members) = grp.getgrnam('sudo') + return current_user in admin_group_members + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('Missing command argument') + sys.exit(1) + + if not is_admin(): + print('This account is not authorized to run this command') + sys.exit(1) + + os.execvp('sudo', ['sudo'] + sys.argv[1:]) diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh new file mode 100755 index 000000000..b013e16f8 --- /dev/null +++ b/src/op_mode/show_ram.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Module: vyos-show-ram.sh +# Displays memory usage information in minimalistic format +# +# 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 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 . + +MB_DIVISOR=1024 + +TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}') +FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}') +BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}') +CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}') + +DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR )) +DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR )) +DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE )) + +echo "Total: $DISPLAY_TOTAL" +echo "Free: $DISPLAY_FREE" +echo "Used: $DISPLAY_USED" diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py new file mode 100755 index 000000000..8e4f12851 --- /dev/null +++ b/src/op_mode/show_users.py @@ -0,0 +1,111 @@ +#!/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 argparse +import pwd +import spwd +import struct +import sys +from time import ctime + +from tabulate import tabulate +from vyos.config import Config + + +class UserInfo: + def __init__(self, uid, name, user_type, is_locked, login_time, tty, host): + self.uid = uid + self.name = name + self.user_type = user_type + self.is_locked = is_locked + self.login_time = login_time + self.tty = tty + self.host = host + + +filters = { + 'default': lambda user: not user.is_locked, # Default is everything but locked accounts + 'vyos': lambda user: user.user_type == 'vyos', + 'other': lambda user: user.user_type != 'vyos', + 'locked': lambda user: user.is_locked, + 'all': lambda user: True +} + + +def is_locked(user_name: str) -> bool: + """Check if a given user has password in shadow db""" + + try: + encrypted_password = spwd.getspnam(user_name)[1] + return encrypted_password == '*' or encrypted_password.startswith('!') + except (KeyError, PermissionError): + print('Cannot access shadow database, ensure this script is run with sufficient permissions') + sys.exit(1) + + +def decode_lastlog(lastlog_file, uid: int): + """Decode last login info of a given user uid from the lastlog file""" + + struct_fmt = '=L32s256s' + recordsize = struct.calcsize(struct_fmt) + lastlog_file.seek(recordsize * uid) + buf = lastlog_file.read(recordsize) + if len(buf) < recordsize: + return None + (time, tty, host) = struct.unpack(struct_fmt, buf) + time = 'never logged in' if time == 0 else ctime(time) + tty = tty.strip(b'\x00') + host = host.strip(b'\x00') + return time, tty, host + + +def list_users(): + cfg = Config() + vyos_users = cfg.list_effective_nodes('system login user') + users = [] + with open('/var/log/lastlog', 'rb') as lastlog_file: + for (name, _, uid, _, _, _, _) in pwd.getpwall(): + lastlog_info = decode_lastlog(lastlog_file, uid) + if lastlog_info is None: + continue + user_info = UserInfo( + uid, name, + user_type='vyos' if name in vyos_users else 'other', + is_locked=is_locked(name), + login_time=lastlog_info[0], + tty=lastlog_info[1], + host=lastlog_info[2]) + users.append(user_info) + return users + + +def main(): + parser = argparse.ArgumentParser(prog=sys.argv[0], add_help=False) + parser.add_argument('type', nargs='?', choices=['all', 'vyos', 'other', 'locked']) + args = parser.parse_args() + + filter_type = args.type if args.type is not None else 'default' + filter_expr = filters[filter_type] + + headers = ['Username', 'Type', 'Locked', 'Tty', 'From', 'Last login'] + table_data = [] + for user in list_users(): + if filter_expr(user): + table_data.append([user.name, user.user_type, user.is_locked, user.tty, user.host, user.login_time]) + print(tabulate(table_data, headers, tablefmt='simple')) + + +if __name__ == '__main__': + main() -- cgit v1.2.3 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 From 700a3f85bf05b93ea0f1ea42d5efcdd4795ae21f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 11:53:58 +0200 Subject: openvpn: T1548: 'disabled' leafNode must be valueless --- interface-definitions/interfaces-openvpn.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml index f2eb1ebab..d4e903c48 100644 --- a/interface-definitions/interfaces-openvpn.xml +++ b/interface-definitions/interfaces-openvpn.xml @@ -99,6 +99,7 @@ Disable interface + -- cgit v1.2.3 From 7f41c331f9023972097ad8e5b375fdf82843e121 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 12:08:58 +0200 Subject: openvpn: T1548: fix enable/disable of entire interface --- src/conf_mode/interface-openvpn.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 339668f5f..67700222c 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -233,7 +233,7 @@ push "route {{ route }}" iroute {{ net }} {% endfor -%} -{%- if disabled %} +{%- if disable %} disable {% endif %} """ @@ -247,7 +247,7 @@ default_config_data = { 'compress_lzo': False, 'deleted': False, 'description': '', - 'disabled': False, + 'disable': False, 'encryption': '', 'hash': '', 'intf': '', @@ -377,9 +377,9 @@ def get_config(): if conf.exists('device-type'): openvpn['type'] = conf.return_value('device-type') - # interface disabled - if conf.exists('disabled'): - openvpn['disabled'] = True + # disable interface + if conf.exists('disable'): + openvpn['disable'] = True # data encryption algorithm if conf.exists('encryption'): @@ -491,7 +491,7 @@ def get_config(): conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client) data = { 'name': client, - 'disabled': False, + 'disable': False, 'ip': '', 'push_route': [], 'subnet': [], @@ -513,7 +513,7 @@ def get_config(): # Option to disable client connection if conf.exists('disable'): - data['disabled'] = True + data['disable'] = True # IP address of the client if conf.exists('ip'): @@ -852,7 +852,7 @@ def apply(openvpn): pid = int(f.read()) # If tunnel interface has been deleted - stop service - if openvpn['deleted']: + if openvpn['deleted'] or openvpn['disable']: directory = os.path.dirname(get_config_name(interface)) # we only need to stop the demon if it's running -- cgit v1.2.3 From fd339a3e45efea527e599fb430ccf538103b2369 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 12:13:07 +0200 Subject: openvpn: T1548: add description to generated config file --- src/conf_mode/interface-openvpn.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 67700222c..1988dcdb1 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -44,6 +44,10 @@ config_tmpl = """ # 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 -- cgit v1.2.3 From c44cb75ac461cf7ef81e47f0bbd9f395abdd5f1e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 18:02:28 +0200 Subject: openvpn: T1548: widen generated folder permission to 755 --- src/conf_mode/interface-openvpn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 1988dcdb1..0f1cebaad 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -305,8 +305,8 @@ def openvpn_mkdir(directory): if not os.path.exists(directory): os.mkdir(directory) - # fix permissions - os.chmod(directory, stat.S_IRWXU|stat.S_IRWXG|stat.S_IROTH) + # fix permissions - corresponds to mode 755 + os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) uid = pwd.getpwnam(user).pw_uid gid = grp.getgrnam(group).gr_gid os.chown(directory, uid, gid) -- cgit v1.2.3 From 26213a792bf5842d30786dfc339df4a622f01181 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 18:03:24 +0200 Subject: openvpn: T1548: fix generated client subnet mask for topology 'server' --- src/conf_mode/interface-openvpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 0f1cebaad..47257e9d7 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -506,7 +506,7 @@ def get_config(): # 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] + 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'/') -- cgit v1.2.3 From 9597c6e9a1ed83cb6cca001ccc08ee743cce9d4c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 18:03:51 +0200 Subject: openvpn: T1548: don't generate config if instance is disabled --- src/conf_mode/interface-openvpn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 47257e9d7..7057d3369 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -796,7 +796,7 @@ def verify(openvpn): return None def generate(openvpn): - if openvpn['deleted']: + if openvpn['deleted'] or openvpn['disable']: return None interface = openvpn['intf'] @@ -844,7 +844,6 @@ def generate(openvpn): return None - def apply(openvpn): interface = openvpn['intf'] -- cgit v1.2.3 From a0bc338338a4518fe45129a5b3160ebb13a809c4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 18:14:08 +0200 Subject: openvpn: T1548: fix generated topology statement for 'server point-to-point' --- src/conf_mode/interface-openvpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 7057d3369..0d85d9643 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -104,7 +104,7 @@ 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 %} +topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} {% endif %} {% for ns in server_dns_nameserver -%} -- cgit v1.2.3 From 66fdca096a268ca574b81b2664078011c5bac3a7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 17 Aug 2019 18:51:00 +0200 Subject: openvpn: T1548: add 'show interfaces openvpn' op-mode command --- op-mode-definitions/openvpn.xml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 4a7f985e9..4c958257a 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -71,4 +71,42 @@ + + + + + + + Show OpenVPN interface information + + + + + Show detailed OpenVPN interface information + + ${vyatta_bindir}/vyatta-show-interfaces.pl --intf-type=openvpn --action=show + + + + + + Show OpenVPN interface information + + + + + ${vyatta_bindir}/vyatta-show-interfaces.pl --intf=$4 + + + + Show summary of specified OpenVPN interface information + + ${vyatta_bindir}/vyatta-show-interfaces.pl --intf="$4" --action=show-brief + + + + + + + -- cgit v1.2.3 From 2367beb1a7c18b60f700afad20fccb8c364bdc1c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 18 Aug 2019 11:25:18 +0200 Subject: [bridge] T1156: increase bridge priority - bridge interfaces after all member interfaces are configured --- interface-definitions/interfaces-bridge.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index d20582849..adb525a46 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -5,7 +5,7 @@ Bridge interface name - 310 + 470 ^br[0-9]+$ -- cgit v1.2.3 From afdde0000333fd720dcf7263cedcb018cf3c3b5f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 18 Aug 2019 11:25:49 +0200 Subject: openvpn: T1548: support creating L2 bridges --- src/conf_mode/interface-openvpn.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 0d85d9643..e4bde7bb0 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -140,9 +140,9 @@ ccd-exclusive ping {{ ping_interval }} ping-restart {{ ping_restart }} -{%- if 'tap' in type %} +{%- if local_address_subnet %} ifconfig {{ local_address }} {{ local_address_subnet }} -{% else %} +{% elif remote_address %} ifconfig {{ local_address }} {{ remote_address }} {% endif %} @@ -638,25 +638,24 @@ def verify(openvpn): 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'] == 'tun': + if not openvpn['remote_address']: + raise ConfigError('Must specify "remote-address"') - if openvpn['type'] == 'tap' and openvpn['local_address_subnet'] == '': - raise ConfigError('Must specify "subnet-mask" for local-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"') 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') -- cgit v1.2.3