summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/completion/list_openvpn_users.py48
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py56
-rwxr-xr-xsrc/op_mode/show_openvpn_mfa.py64
3 files changed, 164 insertions, 4 deletions
diff --git a/src/completion/list_openvpn_users.py b/src/completion/list_openvpn_users.py
new file mode 100755
index 000000000..c472dbeab
--- /dev/null
+++ b/src/completion/list_openvpn_users.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import dict_search
+
+def get_user_from_interface(interface):
+ config = Config()
+ base = ['interfaces', 'openvpn', interface]
+ openvpn = config.get_config_dict(base, effective=True, key_mangling=('-', '_'))
+ users = []
+
+ try:
+ for user in (dict_search('server.client', openvpn[interface]) or []):
+ users.append(user.split(',')[0])
+ except:
+ pass
+
+ return users
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help="List users per interface")
+ args = parser.parse_args()
+
+ users = []
+
+ users = get_user_from_interface(args.interface)
+
+ print(" ".join(users))
+
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index ce62a8b82..7f4aa367f 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -16,6 +16,7 @@
import os
import re
+import tempfile
from cryptography.hazmat.primitives.asymmetric import ec
from glob import glob
@@ -26,6 +27,7 @@ from ipaddress import IPv6Address
from ipaddress import IPv6Network
from ipaddress import summarize_address_range
from netifaces import interfaces
+from secrets import SystemRandom
from shutil import rmtree
from vyos.config import Config
@@ -48,6 +50,7 @@ from vyos.util import chown
from vyos.util import dict_search
from vyos.util import dict_search_args
from vyos.util import makedir
+from vyos.util import read_file
from vyos.util import write_file
from vyos.validate import is_addr_assigned
@@ -60,6 +63,9 @@ group = 'openvpn'
cfg_dir = '/run/openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
+otp_path = '/config/auth/openvpn'
+otp_file = '/config/auth/openvpn/{ifname}-otp-secrets'
+secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
def get_config(config=None):
"""
@@ -75,12 +81,26 @@ def get_config(config=None):
tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
+ # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
+ # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
+ tmp_openvpn = conf.get_config_dict(base + [os.environ['VYOS_TAGNODE_VALUE']], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
openvpn = get_interface_dict(conf, base)
if 'deleted' not in openvpn:
openvpn['pki'] = tmp_pki
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
+
+ # We have to cleanup the config dict, as default values could enable features
+ # which are not explicitly enabled on the CLI. Example: server mfa totp
+ # originate comes with defaults, which will enable the
+ # totp plugin, even when not set via CLI so we
+ # need to check this first and drop those keys
+ if 'totp' not in tmp_openvpn['server']:
+ del openvpn['server']['mfa']['totp']
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -169,6 +189,10 @@ def verify_pki(openvpn):
def verify(openvpn):
if 'deleted' in openvpn:
+ # remove totp secrets file if totp is not configured
+ if os.path.isfile(otp_file.format(**openvpn)):
+ os.remove(otp_file.format(**openvpn))
+
verify_bridge_delete(openvpn)
return None
@@ -309,10 +333,10 @@ def verify(openvpn):
if 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
-
- for client in (dict_search('client', openvpn) or []):
- if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
- raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
+ if hasattr(dict_search('server.client', openvpn), '__iter__'):
+ for client_k, client_v in dict_search('server.client', openvpn).items():
+ if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1):
+ raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
if dict_search('server.client_ip_pool', openvpn):
if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
@@ -360,6 +384,29 @@ def verify(openvpn):
if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
+ # add mfa users to the file the mfa plugin uses
+ if dict_search('server.mfa.totp', openvpn):
+ user_data = ''
+ if not os.path.isfile(otp_file.format(**openvpn)):
+ write_file(otp_file.format(**openvpn), user_data,
+ user=user, group=group, mode=0o644)
+
+ ovpn_users = read_file(otp_file.format(**openvpn))
+ for client in (dict_search('server.client', openvpn) or []):
+ exists = None
+ for ovpn_user in ovpn_users.split('\n'):
+ if re.search('^' + client + ' ', ovpn_user):
+ user_data += f'{ovpn_user}\n'
+ exists = 'true'
+
+ if not exists:
+ random = SystemRandom()
+ totp_secret = ''.join(random.choice(secret_chars) for _ in range(16))
+ user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n'
+
+ write_file(otp_file.format(**openvpn), user_data,
+ user=user, group=group, mode=0o644)
+
else:
# checks for both client and site-to-site go here
if dict_search('server.reject_unconfigured_clients', openvpn):
@@ -525,6 +572,7 @@ def generate_pki_files(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
+ plugin_dir = '/usr/lib/openvpn'
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
diff --git a/src/op_mode/show_openvpn_mfa.py b/src/op_mode/show_openvpn_mfa.py
new file mode 100755
index 000000000..1ab54600c
--- /dev/null
+++ b/src/op_mode/show_openvpn_mfa.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+# Copyright 2017, 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import re
+import socket
+import urllib.parse
+import argparse
+
+from vyos.util import popen
+
+otp_file = '/config/auth/openvpn/{interface}-otp-secrets'
+
+def get_mfa_secret(interface, client):
+ try:
+ with open(otp_file.format(interface=interface), "r") as f:
+ users = f.readlines()
+ for user in users:
+ if re.search('^' + client + ' ', user):
+ return user.split(':')[3]
+ except:
+ pass
+
+def get_mfa_uri(client, secret):
+ hostname = socket.gethostname()
+ fqdn = socket.getfqdn()
+ uri = 'otpauth://totp/{hostname}:{client}@{fqdn}?secret={secret}'
+
+ return urllib.parse.quote(uri.format(hostname=hostname, client=client, fqdn=fqdn, secret=secret), safe='/:@?=')
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(add_help=False, description='Show two-factor authentication information')
+ parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface')
+ parser.add_argument('--user', action="store", type=str, default='', help='only show the specified users')
+ parser.add_argument('--action', action="store", type=str, default='show', help='action to perform')
+
+ args = parser.parse_args()
+ secret = get_mfa_secret(args.intf, args.user)
+
+ if args.action == "secret" and secret:
+ print(secret)
+
+ if args.action == "uri" and secret:
+ uri = get_mfa_uri(args.user, secret)
+ print(uri)
+
+ if args.action == "qrcode" and secret:
+ uri = get_mfa_uri(args.user, secret)
+ qrcode,err = popen('qrencode -t ansiutf8', input=uri)
+ print(qrcode)
+