From 6748dbe0100cfedf1b2f00884899e71729bfa9f3 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Tue, 17 Aug 2021 07:04:34 -0500 Subject: add part 2fa --- src/conf_mode/interfaces-openvpn.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 74e29ed82..f19804910 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -25,7 +25,9 @@ from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range +from pathlib import Path from netifaces import interfaces +from secrets import SystemRandom from shutil import rmtree from vyos.config import Config @@ -309,9 +311,9 @@ def verify(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') + for client_k, client_v in (dict_search('server.client', openvpn).items() or []): + 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)): @@ -359,6 +361,23 @@ 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.') + if dict_search('server.2fa.totp', openvpn): + if not Path(otp_file).is_file(): + Path(otp_file).touch() + for client in (dict_search('server.client', openvpn) or []): + with open(otp_file, "r+") as f: + users = f.readlines() + exists = None + for user in users: + if re.search('^' + client + ' ', user): + exists = 'true' + + if not exists: + random = SystemRandom() + totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) + f.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) + + else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): -- cgit v1.2.3 From 02b6370c3cd1b580b0140deed6c250a682c3a4eb Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Wed, 1 Sep 2021 14:09:55 -0500 Subject: more 2fa changes --- interface-definitions/interfaces-openvpn.xml.in | 41 ++++++++++++++++++++++++- op-mode-definitions/openvpn.xml.in | 28 +++++++++++++++++ src/conf_mode/interfaces-openvpn.py | 12 +++++--- 3 files changed, 75 insertions(+), 6 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 1a07e7d91..b9575595c 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -648,32 +648,71 @@ Maximum allowed clock slop in seconds (default: 180) + + 1-65535 + Seconds + + + + 180 time drift in seconds (default: 0) + + 1-65535 + Seconds + + + + 0 Step value for TOTP in seconds (default: 30) + + 1-65535 + Seconds + + + + 30 Number of digits to use from TOTP hash (default: 6) + + 1-65535 + Seconds + + + + 6 expect password as result of a challenge response protocol (default: enabled) + + disable enable + + + disable + Disable challenge response (default) + + + enable + Enable chalenge response (default) + - ^(enable|disable)$ + ^(disable|enable)$ enable diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 781fbdc9d..ee3b073b5 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -55,6 +55,34 @@ ${vyos_op_scripts_dir}/show_interfaces.py --intf=$4 + + + Show OpenVPN interface users + + + + + + + + Show 2fa authentication secret + + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=sercret + + + + Show 2fa otpauth uri + + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=uri + + + + Show 2fa QR code + + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=qrcode + + + Show summary of specified OpenVPN interface information diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index f19804910..8ccfee6ef 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -61,6 +61,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): """ @@ -310,7 +313,6 @@ 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_k, client_v in (dict_search('server.client', openvpn).items() or []): 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') @@ -362,10 +364,11 @@ def verify(openvpn): print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.') if dict_search('server.2fa.totp', openvpn): - if not Path(otp_file).is_file(): - Path(otp_file).touch() + if not Path(otp_file.format(**openvpn)).is_file(): + Path(otp_path).mkdir(parents=True, exist_ok=True) + Path(otp_file.format(**openvpn)).touch() for client in (dict_search('server.client', openvpn) or []): - with open(otp_file, "r+") as f: + with open(otp_file.format(**openvpn), "r+") as f: users = f.readlines() exists = None for user in users: @@ -377,7 +380,6 @@ def verify(openvpn): totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) f.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) - else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): -- cgit v1.2.3 From 04e87d5a597451ea5eb21294666eef31b4daab09 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Thu, 2 Sep 2021 05:01:22 -0500 Subject: update 2fa qr generation and user creation procedure --- src/conf_mode/interfaces-openvpn.py | 44 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 8ccfee6ef..efab07ddc 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 @@ -313,9 +314,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_k, client_v in (dict_search('server.client', openvpn).items() or []): - 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 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)): @@ -363,22 +365,34 @@ 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 2fa users to the file the 2fa plugin uses if dict_search('server.2fa.totp', openvpn): if not Path(otp_file.format(**openvpn)).is_file(): Path(otp_path).mkdir(parents=True, exist_ok=True) Path(otp_file.format(**openvpn)).touch() - for client in (dict_search('server.client', openvpn) or []): - with open(otp_file.format(**openvpn), "r+") as f: - users = f.readlines() - exists = None - for user in users: - if re.search('^' + client + ' ', user): - exists = 'true' - - if not exists: - random = SystemRandom() - totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) - f.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) + + with tempfile.TemporaryFile(mode='w+') as fp: + with open(otp_file.format(**openvpn), 'r+') as f: + ovpn_users = f.readlines() + for client in (dict_search('server.client', openvpn) or []): + exists = None + for ovpn_user in ovpn_users: + if re.search('^' + client + ' ', user): + fp.write(ovpn_user) + exists = 'true' + + if not exists: + random = SystemRandom() + totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) + fp.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) + + f.seek(0) + fp.seek(0) + for tmp_user in fp.readlines(): + f.write(tmp_user) + f.truncate() + + chown(otp_file.format(**openvpn), user, group) else: # checks for both client and site-to-site go here -- cgit v1.2.3 From e3c71af1466da42403fa23bc23e7e530df71c6c8 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Thu, 2 Sep 2021 09:58:42 -0500 Subject: remove secrets file if the tunnel is deleted and fix opmode commands --- op-mode-definitions/openvpn.xml.in | 14 +++++++------- src/conf_mode/interfaces-openvpn.py | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'src/conf_mode') diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index ee3b073b5..6549976c5 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -59,27 +59,27 @@ Show OpenVPN interface users - + - + Show 2fa authentication secret - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=sercret + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=secret - + Show 2fa otpauth uri - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=uri + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=uri - + Show 2fa QR code - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$4" --intf="$6" --action=qrcode + ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=qrcode diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index efab07ddc..194126a34 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -177,6 +177,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 -- cgit v1.2.3 From 65b55f35f836185ffbf589c5ea5a6ee89568957e Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Fri, 3 Sep 2021 03:23:28 -0500 Subject: fix file location and use correct variable --- data/templates/openvpn/server.conf.tmpl | 2 +- src/conf_mode/interfaces-openvpn.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 4c5dbc2c5..e6dd9fcbc 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -133,7 +133,7 @@ plugin "/usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-otp.so" "otp_secrets=/ {{- server['2fa']['totp']['t0']|default(0) }} totp_step= {{- server['2fa']['totp']['step']|default(30) }} totp_digits= {{- server['2fa']['totp']['digits']|default(6)}} password_is_cr= -{%-if server['2fa']['totp']['challenge']|default('enabled') == 'enabled' %}1{% else %}0{% endif %}" +{%-if server['2fa']['totp']['challenge']|default('enable') == 'enable' %}1{% else %}0{% endif %}" {% endif %} {% endif %} {% else %} diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 194126a34..365d0982e 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -381,7 +381,7 @@ def verify(openvpn): for client in (dict_search('server.client', openvpn) or []): exists = None for ovpn_user in ovpn_users: - if re.search('^' + client + ' ', user): + if re.search('^' + client + ' ', ovpn_user): fp.write(ovpn_user) exists = 'true' -- cgit v1.2.3 From ba8630da96396f09c638fccdc9cfe6a3ee70fd58 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Thu, 7 Oct 2021 08:44:00 -0500 Subject: pull request fixes --- data/templates/openvpn/server.conf.tmpl | 12 ++--- interface-definitions/interfaces-openvpn.xml.in | 23 +++++---- op-mode-definitions/openvpn.xml.in | 16 +++---- src/conf_mode/interfaces-openvpn.py | 18 ++++++- src/op_mode/show_openvpn_2fa.py | 64 ------------------------- src/op_mode/show_openvpn_mfa.py | 64 +++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 91 deletions(-) delete mode 100755 src/op_mode/show_openvpn_2fa.py create mode 100755 src/op_mode/show_openvpn_mfa.py (limited to 'src/conf_mode') diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 644eb805f..3104203ad 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -127,14 +127,10 @@ push "dhcp-option DNS6 {{ nameserver }}" {% if server.domain_name is defined and server.domain_name is not none %} push "dhcp-option DOMAIN {{ server.domain_name }}" {% endif %} -{% if server['2fa'] is defined and server['2fa'] is not none %} -{% if server['2fa']['totp'] is defined and server['2fa']['totp'] is not none %} -plugin "/usr/lib/openvpn/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ ifname }}-otp-secrets otp_slop= -{{- server['2fa']['totp']['slop']|default(180) }} totp_t0= -{{- server['2fa']['totp']['drift']|default(0) }} totp_step= -{{- server['2fa']['totp']['step']|default(30) }} totp_digits= -{{- server['2fa']['totp']['digits']|default(6)}} password_is_cr= -{%-if server['2fa']['totp']['challenge']|default('enable') == 'enable' %}1{% else %}0{% endif %}" +{% if server.mfa is defined and server.mfa is not none %} +{% if server.mfa.totp is defined and server.mfa.totp is not none %} +{% set totp_config = server.mfa.totp %} +plugin "{{ plugin_dir}}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ ifname }}-otp-secrets {{ 'otp_slop=' ~ totp_config.slop }} {{ 'totp_t0=' ~ totp_config.drift }} {{ 'totp_step=' ~ totp_config.step }} {{ 'totp_digits=' ~ totp_config.digits }} password_is_cr={{ '1' if totp_config.challenge == 'enable' else '0' }}" {% endif %} {% endif %} {% endif %} diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 62fac9be0..023f9f55d 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -635,14 +635,14 @@ net30 - + - 2-factor authentication + multi-factor authentication - Time-based One-Time Passwords + Time-based one-time passwords @@ -656,10 +656,11 @@ + 180 - time drift in seconds (default: 0) + Time drift in seconds (default: 0) 1-65535 Seconds @@ -668,10 +669,11 @@ + 0 - Step value for TOTP in seconds (default: 30) + Step value for totp in seconds (default: 30) 1-65535 Seconds @@ -680,10 +682,11 @@ + 30 - Number of digits to use from TOTP hash (default: 6) + Number of digits to use for totp hash (default: 6) 1-65535 Seconds @@ -692,25 +695,27 @@ + 6 - expect password as result of a challenge response protocol (default: enabled) + Expect password as result of a challenge response protocol (default: enabled) disable enable disable - Disable challenge response (default) + Disable challenge-response enable - Enable chalenge response (default) + Enable chalenge-response (default) ^(disable|enable)$ + enable diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 068d5d8fb..7243d69fd 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -63,28 +63,28 @@ - + - Show 2fa information + Show multi-factor authentication information - Show 2fa authentication secret + Show multi-factor authentication secret - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=secret + ${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=secret - Show 2fa otpauth uri + Show multi-factor authentication otpauth uri - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=uri + ${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=uri - Show 2fa QR code + Show multi-factor authentication QR code - ${vyos_op_scripts_dir}/show_openvpn_2fa.py --user="$6" --intf="$4" --action=qrcode + ${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=qrcode diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 365d0982e..220c4f157 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -80,6 +80,11 @@ 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: @@ -89,6 +94,14 @@ def get_config(config=None): openvpn['daemon_user'] = user openvpn['daemon_group'] = group + # 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): @@ -369,8 +382,8 @@ 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 2fa users to the file the 2fa plugin uses - if dict_search('server.2fa.totp', openvpn): + # add mfa users to the file the mfa plugin uses + if dict_search('server.mfa.totp', openvpn): if not Path(otp_file.format(**openvpn)).is_file(): Path(otp_path).mkdir(parents=True, exist_ok=True) Path(otp_file.format(**openvpn)).touch() @@ -590,6 +603,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' # we can't know in advance which clients have been removed, # thus all client configs will be removed and re-added on demand diff --git a/src/op_mode/show_openvpn_2fa.py b/src/op_mode/show_openvpn_2fa.py deleted file mode 100755 index 8600f755d..000000000 --- a/src/op_mode/show_openvpn_2fa.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2017, 2021 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 re -import socket -import urllib.parse -import argparse - -from vyos.util import popen - -otp_file = '/config/auth/openvpn/{interface}-otp-secrets' - -def get_2fa_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_2fa_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 2fa 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_2fa_secret(args.intf, args.user) - - if args.action == "secret" and secret: - print(secret) - - if args.action == "uri" and secret: - uri = get_2fa_uri(args.user, secret) - print(uri) - - if args.action == "qrcode" and secret: - uri = get_2fa_uri(args.user, secret) - qrcode,err = popen('qrencode -t ansiutf8', input=uri) - print(qrcode) - 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 +# +# 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 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) + -- cgit v1.2.3 From 9cd3c3bfe04b6fe96df04092768e657c144b2157 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:33:14 +0200 Subject: openvpn: T3805: use vyos.util.write_file() to store certificates --- python/vyos/util.py | 4 +-- src/conf_mode/interfaces-openvpn.py | 58 ++++++++++--------------------------- 2 files changed, 18 insertions(+), 44 deletions(-) (limited to 'src/conf_mode') diff --git a/python/vyos/util.py b/python/vyos/util.py index 59f9f1c44..05643a223 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -197,7 +197,7 @@ def read_file(fname, defaultonfailure=None): return defaultonfailure raise e -def write_file(fname, data, defaultonfailure=None, user=None, group=None): +def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None): """ Write content of data to given fname, should defaultonfailure be not None, it is returned on failure to read. @@ -215,6 +215,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None): with open(fname, 'w') as f: bytes = f.write(data) chown(fname, user, group) + chmod(fname, mode) return bytes except Exception as e: if defaultonfailure is not None: @@ -295,7 +296,6 @@ def makedir(path, user=None, group=None): os.makedirs(path, mode=0o755) chown(path, user, group) - def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries of colon-separated key-value pairs into a dict. diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 220c4f157..6918c46e0 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -48,9 +48,9 @@ from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call from vyos.util import chown -from vyos.util import chmod_600 from vyos.util import dict_search from vyos.util import dict_search_args +from vyos.util import write_file from vyos.validate import is_addr_assigned from vyos import ConfigError @@ -498,7 +498,6 @@ def verify(openvpn): def generate_pki_files(openvpn): pki = openvpn['pki'] - if not pki: return None @@ -506,16 +505,11 @@ def generate_pki_files(openvpn): shared_secret_key = dict_search_args(openvpn, 'shared_secret_key') tls = dict_search_args(openvpn, 'tls') - files = [] - if shared_secret_key: pki_key = pki['openvpn']['shared_secret'][shared_secret_key] key_path = os.path.join(cfg_dir, f'{interface}_shared.key') - - with open(key_path, 'w') as f: - f.write(wrap_openvpn_key(pki_key['key'])) - - files.append(key_path) + write_file(key_path, wrap_openvpn_key(pki_key['key']), + user=user, group=group) if tls: if 'ca_certificate' in tls: @@ -524,20 +518,15 @@ def generate_pki_files(openvpn): if 'certificate' in pki_ca: cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') - - with open(cert_path, 'w') as f: - f.write(wrap_certificate(pki_ca['certificate'])) - - files.append(cert_path) + write_file(cert_path, wrap_certificate(pki_ca['certificate']), + user=user, group=group, mode=0o600) if 'crl' in pki_ca: for crl in pki_ca['crl']: crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') + write_file(crl_path, wrap_crl(crl), user=user, group=group, + mode=0o600) - with open(crl_path, 'w') as f: - f.write(wrap_crl(crl)) - - files.append(crl_path) openvpn['tls']['crl'] = True if 'certificate' in tls: @@ -546,19 +535,14 @@ def generate_pki_files(openvpn): if 'certificate' in pki_cert: cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem') - - with open(cert_path, 'w') as f: - f.write(wrap_certificate(pki_cert['certificate'])) - - files.append(cert_path) + write_file(cert_path, wrap_certificate(pki_cert['certificate']), + user=user, group=group, mode=0o600) if 'private' in pki_cert and 'key' in pki_cert['private']: key_path = os.path.join(cfg_dir, f'{interface}_cert.key') + write_file(key_path, wrap_private_key(pki_cert['private']['key']), + user=user, group=group, mode=0o600) - with open(key_path, 'w') as f: - f.write(wrap_private_key(pki_cert['private']['key'])) - - files.append(key_path) openvpn['tls']['private_key'] = True if 'dh_params' in tls: @@ -567,11 +551,8 @@ def generate_pki_files(openvpn): if 'parameters' in pki_dh: dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem') - - with open(dh_path, 'w') as f: - f.write(wrap_dh_parameters(pki_dh['parameters'])) - - files.append(dh_path) + write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']), + user=user, group=group, mode=0o600) if 'auth_key' in tls: key_name = tls['auth_key'] @@ -579,11 +560,8 @@ def generate_pki_files(openvpn): if 'key' in pki_key: key_path = os.path.join(cfg_dir, f'{interface}_auth.key') - - with open(key_path, 'w') as f: - f.write(wrap_openvpn_key(pki_key['key'])) - - files.append(key_path) + write_file(key_path, wrap_openvpn_key(pki_key['key']), + user=user, group=group, mode=0o600) if 'crypt_key' in tls: key_name = tls['crypt_key'] @@ -620,7 +598,7 @@ def generate(openvpn): chown(ccd_dir, user, group) # Fix file permissons for keys - fix_permissions = generate_pki_files(openvpn) + generate_pki_files(openvpn) # Generate User/Password authentication file if 'authentication' in openvpn: @@ -648,10 +626,6 @@ def generate(openvpn): render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) - # Fixup file permissions - for file in fix_permissions: - chmod_600(file) - return None def apply(openvpn): -- cgit v1.2.3 From 2349f2d91213b702394e9ca72aa2e6d4ee8c0dae Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:34:41 +0200 Subject: openvpn: T3805: use vyos.util.makedir() to create system directories --- src/conf_mode/interfaces-openvpn.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 6918c46e0..94fb14246 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -50,6 +50,7 @@ from vyos.util import call 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 write_file from vyos.validate import is_addr_assigned @@ -569,19 +570,18 @@ def generate_pki_files(openvpn): if 'key' in pki_key: key_path = os.path.join(cfg_dir, f'{interface}_crypt.key') - - with open(key_path, 'w') as f: - f.write(wrap_openvpn_key(pki_key['key'])) - - files.append(key_path) - - return files + write_file(key_path, wrap_openvpn_key(pki_key['key']), + user=user, group=group, mode=0o600) 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 + chown(directory, user, group) # we can't know in advance which clients have been removed, # thus all client configs will be removed and re-added on demand @@ -593,9 +593,7 @@ def generate(openvpn): return None # create client config directory on demand - if not os.path.exists(ccd_dir): - os.makedirs(ccd_dir, 0o755) - chown(ccd_dir, user, group) + makedir(ccd_dir, user, group) # Fix file permissons for keys generate_pki_files(openvpn) -- cgit v1.2.3 From 699d4533c543f2578c68f1d3ca9f2a2b8d5c4692 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:35:20 +0200 Subject: openvpn: T3805: drop privileges using systemd - required for rtnetlink --- data/templates/openvpn/server.conf.tmpl | 2 -- src/conf_mode/interfaces-openvpn.py | 2 -- src/etc/systemd/system/openvpn@.service.d/override.conf | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 3104203ad..5c78d998e 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -7,8 +7,6 @@ # verb 3 -user {{ daemon_user }} -group {{ daemon_group }} dev-type {{ device_type }} dev {{ ifname }} persist-key diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 94fb14246..d57ccb354 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -92,8 +92,6 @@ def get_config(config=None): openvpn['pki'] = tmp_pki openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) - openvpn['daemon_user'] = user - openvpn['daemon_group'] = group # 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 diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf index 7946484a3..03fe6b587 100644 --- a/src/etc/systemd/system/openvpn@.service.d/override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/override.conf @@ -7,3 +7,7 @@ WorkingDirectory= WorkingDirectory=/run/openvpn ExecStart= ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +User=openvpn +Group=openvpn +AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE +CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE -- cgit v1.2.3 From c567b43807faa09e3bee748d06d31619f5e97aa9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 8 Sep 2021 14:36:06 +0200 Subject: openvpn: T3805: fix bool logic in verify_pki() for client mode Add support for OpenVPN client mode with only the CA certificate of the server installed. --- src/conf_mode/interfaces-openvpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index d57ccb354..ed4a6f77d 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -146,7 +146,7 @@ def verify_pki(openvpn): if tls['ca_certificate'] not in pki['ca']: raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') - if not (mode == 'client' and 'auth_key' in tls): + if mode != 'client' and 'auth_key' not in tls: if 'certificate' not in tls: raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}') -- cgit v1.2.3 From 2acfffab8b98238e7d869673a858a4ae21651f0b Mon Sep 17 00:00:00 2001 From: Nicolas Riebesel Date: Thu, 23 Sep 2021 01:28:22 +0200 Subject: openvpn: T3642: Fix password_protected check --- src/conf_mode/interfaces-openvpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index ed4a6f77d..4bd0b22a9 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -154,7 +154,7 @@ def verify_pki(openvpn): if tls['certificate'] not in pki['certificate']: raise ConfigError(f'Invalid certificate on openvpn interface {interface}') - if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected'): + if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None: raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']): -- cgit v1.2.3 From 5aadf673497b93e2d4ad304e567de1cd571f9e25 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 8 Oct 2021 21:17:52 +0200 Subject: tunnel: T3893: harden logic when validating tunnel parameters Different types of tunnels have different keys set in get_interface_config(). Thus it should be properly verified (by e.g. using dict_search()) that the key in question esits to not raise KeyError. --- src/conf_mode/interfaces-tunnel.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index ef385d2e7..51127127d 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -108,18 +108,17 @@ def verify(tunnel): # Prevent the same key for 2 tunnels with same source-address/encap. T2920 for tunnel_if in Section.interfaces('tunnel'): tunnel_cfg = get_interface_config(tunnel_if) - exist_encap = tunnel_cfg['linkinfo']['info_kind'] - exist_source_address = tunnel_cfg['address'] - exist_key = tunnel_cfg['linkinfo']['info_data']['ikey'] + # no match on encapsulation - bail out + if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: + continue new_source_address = tunnel['source_address'] # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 - orig_new_key = int(tunnel['parameters']['ip']['key']) - new_key = IPv4Address(orig_new_key) + orig_new_key = dict_search('parameters.ip.key', tunnel) + new_key = IPv4Address(int(orig_new_key)) new_key = str(new_key) - if tunnel['encapsulation'] == exist_encap and \ - new_source_address == exist_source_address and \ - new_key == exist_key: + if dict_search('address', tunnel_cfg) == new_source_address and \ + dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ f'is already used for tunnel "{tunnel_if}"!') -- cgit v1.2.3 From f8dca381b4822f218b4664e89b73ea2929f90a03 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Sun, 10 Oct 2021 09:43:56 -0500 Subject: update writer to nicer read write --- src/conf_mode/interfaces-openvpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 2533a5b02..7cfed0c55 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -403,7 +403,7 @@ def verify(openvpn): if not exists: random = SystemRandom() totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) - fp.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) + fp.write(f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n') f.seek(0) fp.seek(0) -- cgit v1.2.3 From d2c17f9864d26b7adc6c9f21dbe46f1d7059dbb4 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Sun, 10 Oct 2021 10:50:41 -0500 Subject: do not use Path --- src/conf_mode/interfaces-openvpn.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 7cfed0c55..2c8df4831 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -26,7 +26,6 @@ from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range -from pathlib import Path from netifaces import interfaces from secrets import SystemRandom from shutil import rmtree @@ -386,9 +385,9 @@ def verify(openvpn): # add mfa users to the file the mfa plugin uses if dict_search('server.mfa.totp', openvpn): - if not Path(otp_file.format(**openvpn)).is_file(): - Path(otp_path).mkdir(parents=True, exist_ok=True) - Path(otp_file.format(**openvpn)).touch() + if not os.path.isfile(otp_file.format(**openvpn)): + makedir(otp_path) + open(otp_file.format(**openvpn), 'a').close() with tempfile.TemporaryFile(mode='w+') as fp: with open(otp_file.format(**openvpn), 'r+') as f: -- cgit v1.2.3 From 46e331cdf44b7880f6e2f5fefaa1f536829f6ebc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 11 Oct 2021 16:47:08 +0200 Subject: vyos.configdict: T2653: do not merge in defaults when interface is deleted It makes less to zero sense to blend in the default values of an interface when it is about to be deleted from the system anyways - this makes the entire dict just cleaner and easier to debug. --- python/vyos/configdict.py | 38 +++++++++++++++++++++++------------- src/conf_mode/interfaces-wireless.py | 5 +++-- 2 files changed, 27 insertions(+), 16 deletions(-) (limited to 'src/conf_mode') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 5c6836e97..8308f0e9b 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -368,9 +368,11 @@ def get_interface_dict(config, base, ifname=''): del default_values['dhcpv6_options'] # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - dict = dict_merge(default_values, dict) + # default options which we need to update into the dictionary retrived. + # But we should only add them when interface is not deleted - as this might + # confuse parsers + if 'deleted' not in dict: + dict = dict_merge(default_values, dict) # XXX: T2665: blend in proper DHCPv6-PD default values dict = T2665_set_dhcpv6pd_defaults(dict) @@ -423,9 +425,12 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_config: del default_vif_values['dhcpv6_options'] - dict['vif'][vif] = dict_merge(default_vif_values, vif_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + dict['vif'][vif] = dict_merge(default_vif_values, vif_config) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif}', 'bridge') @@ -441,10 +446,12 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_s_config: del default_vif_s_values['dhcpv6_options'] - dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults( - dict['vif_s'][vif_s]) + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge') @@ -458,11 +465,14 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_c_config: del default_vif_c_values['dhcpv6_options'] - dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( default_vif_c_values, vif_c_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( - dict['vif_s'][vif_s]['vif_c'][vif_c]) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( + dict['vif_s'][vif_s]['vif_c'][vif_c]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 7b3de6e8a..af35b5f03 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -82,11 +82,12 @@ def get_config(config=None): tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) if not (dict_search('security.wpa.passphrase', tmp) or dict_search('security.wpa.radius', tmp)): - del wifi['security']['wpa'] + if 'deleted' not in wifi: + del wifi['security']['wpa'] # defaults include RADIUS server specifics per TAG node which need to be # added to individual RADIUS servers instead - so we can simply delete them - if dict_search('security.wpa.radius.server.port', wifi): + if dict_search('security.wpa.radius.server.port', wifi) != None: del wifi['security']['wpa']['radius']['server']['port'] if not len(wifi['security']['wpa']['radius']['server']): del wifi['security']['wpa']['radius'] -- cgit v1.2.3 From a633bdd2ed65971b2f137d5f985f8f3d85b9acfc Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 15 Oct 2021 18:18:39 +0000 Subject: containers: T3676: Allow to set capabilities --- interface-definitions/containers.xml.in | 24 ++++++++++++++++++++++++ src/conf_mode/containers.py | 10 +++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in index fb8241d71..24d1870af 100644 --- a/interface-definitions/containers.xml.in +++ b/interface-definitions/containers.xml.in @@ -21,6 +21,30 @@ + + + Add capabilities + + net-admin setpcap sys-time + + + net-admin + Net-admin option + + + setpcap + Setpcap option + + + sys-time + Sys-time option + + + ^(net-admin|setpcap|sys-time)$ + + + + #include #include diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 1e0197a13..cc34f9d39 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -271,6 +271,14 @@ def apply(container): tmp = run(f'podman image exists {image}') if tmp != 0: print(os.system(f'podman pull {image}')) + # Add capability options. Should be in uppercase + cap_add = '' + if 'cap_add' in container_config: + for c in container_config['cap_add']: + c = c.upper() + c = c.replace('-', '_') + cap_add += f' --cap-add={c}' + # Check/set environment options "-e foo=bar" env_opt = '' if 'environment' in container_config: @@ -299,7 +307,7 @@ def apply(container): dvol = vol_config['destination'] volume += f' -v {svol}:{dvol}' - container_base_cmd = f'podman run --detach --interactive --tty --replace ' \ + container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {port} {volume} {env_opt}' if 'allow_host_networks' in container_config: -- cgit v1.2.3 From 3d00140453b3967370c77ddd9dac4af223a7ddce Mon Sep 17 00:00:00 2001 From: Marek Isalski Date: Fri, 6 Aug 2021 14:44:48 +0100 Subject: l2tp: T3724: allow setting accel-ppp l2tp host-name --- data/templates/accel-ppp/l2tp.config.tmpl | 3 +++ interface-definitions/vpn_l2tp.xml.in | 5 +++++ src/conf_mode/vpn_l2tp.py | 2 ++ 3 files changed, 10 insertions(+) (limited to 'src/conf_mode') diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index 44c96b935..9fcda76d4 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -57,6 +57,9 @@ bind={{ outside_addr }} {% if lns_shared_secret %} secret={{ lns_shared_secret }} {% endif %} +{% if lns_host_name %} +host-name={{ lns_host_name }} +{% endif %} [client-ip-range] 0.0.0.0/0 diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index cbd5e38e7..6cb0d4544 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -34,6 +34,11 @@ Tunnel password used to authenticate the client (LAC) + + + Sent to the client (LAC) in the Host-Name attribute + + diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 9c52f77ca..818e8fa0b 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -290,6 +290,8 @@ def get_config(config=None): # LNS secret if conf.exists(['lns', 'shared-secret']): l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + if conf.exists(['lns', 'host-name']): + l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name']) if conf.exists(['ccp-disable']): l2tp['ccp_disable'] = True -- cgit v1.2.3 From 35aeea69c62a1755595d34b856d03f58cdd2da4c Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 11 Oct 2021 06:32:07 +0000 Subject: ddclient: T3897: Add option for IPv6 Dynamic DNS --- data/templates/dynamic-dns/ddclient.conf.tmpl | 2 +- interface-definitions/dns-dynamic.xml.in | 6 ++++ smoketest/scripts/cli/test_service_dns_dynamic.py | 38 +++++++++++++++++++++++ src/conf_mode/dynamic_dns.py | 2 +- 4 files changed, 46 insertions(+), 2 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl index 9d379de00..517e4bad4 100644 --- a/data/templates/dynamic-dns/ddclient.conf.tmpl +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -9,7 +9,7 @@ ssl=yes {% set web_skip = ", web-skip='" + interface[iface].use_web.skip + "'" if interface[iface].use_web.skip is defined else '' %} use=web, web='{{ interface[iface].use_web.url }}'{{ web_skip }} {% else %} -use=if, if={{ iface }} +{{ 'usev6=if' if interface[iface].ipv6_enable is defined else 'use=if' }}, if={{ iface }} {% endif %} {% if interface[iface].rfc2136 is defined and interface[iface].rfc2136 is not none %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 250642691..64826516e 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -274,6 +274,12 @@ + + + Allow explicit IPv6 addresses for Dynamic DNS for this interface + + + diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index d8a87ffd4..03fccf2c7 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -24,6 +24,7 @@ from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running +from vyos.util import read_file PROCESS_NAME = 'ddclient' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' @@ -122,5 +123,42 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dyndns_ipv6(self): + ddns = ['interface', 'eth0', 'service', 'dynv6'] + hostname = 'test.ddns.vyos.io' + proto = 'dyndns2' + user = 'none' + password = 'paSS_4ord' + servr = 'ddns.vyos.io' + + self.cli_set(base_path + ['interface', 'eth0', 'ipv6-enable']) + self.cli_set(base_path + ddns + ['host-name', hostname]) + self.cli_set(base_path + ddns + ['login', user]) + self.cli_set(base_path + ddns + ['password', password]) + self.cli_set(base_path + ddns + ['protocol', proto]) + self.cli_set(base_path + ddns + ['server', servr]) + + # commit changes + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + config = read_file(DDCLIENT_CONF) + + protocol = get_config_value('protocol') + login = get_config_value('login') + pwd = get_config_value('password') + server = get_config_value('server') + + # Check some generating config parameters + self.assertTrue(protocol == proto) + self.assertTrue(login == user) + self.assertTrue(pwd == "'" + password + "'") + self.assertTrue(server == servr) + + self.assertIn('usev6=if', config) + self.assertIn(hostname, config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index c979feca7..646de6324 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -132,7 +132,7 @@ def generate(dyndns): return None render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, - permission=0o600) + permission=0o644) return None -- cgit v1.2.3 From 16ae2933ff976737e52113105228a5f7f75686a3 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sat, 16 Oct 2021 20:54:34 +0200 Subject: mdns: T3917: Change to avahi-daemon for IPv4 + IPv6 mDNS repeater --- data/templates/mdns-repeater/avahi-daemon.tmpl | 18 ++++++++++++++++++ data/templates/mdns-repeater/mdns-repeater.tmpl | 2 -- debian/control | 2 +- smoketest/scripts/cli/test_service_mdns-repeater.py | 2 +- src/conf_mode/service_mdns-repeater.py | 10 +++++----- 5 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 data/templates/mdns-repeater/avahi-daemon.tmpl delete mode 100644 data/templates/mdns-repeater/mdns-repeater.tmpl (limited to 'src/conf_mode') diff --git a/data/templates/mdns-repeater/avahi-daemon.tmpl b/data/templates/mdns-repeater/avahi-daemon.tmpl new file mode 100644 index 000000000..65bb5a306 --- /dev/null +++ b/data/templates/mdns-repeater/avahi-daemon.tmpl @@ -0,0 +1,18 @@ +[server] +use-ipv4=yes +use-ipv6=yes +allow-interfaces={{ interface | join(', ') }} +disallow-other-stacks=no + +[wide-area] +enable-wide-area=yes + +[publish] +disable-publishing=yes +disable-user-service-publishing=yes +publish-addresses=no +publish-hinfo=no +publish-workstation=no + +[reflector] +enable-reflector=yes diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl deleted file mode 100644 index 80f4ab047..000000000 --- a/data/templates/mdns-repeater/mdns-repeater.tmpl +++ /dev/null @@ -1,2 +0,0 @@ -### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interface | join(' ') }}" diff --git a/debian/control b/debian/control index 6c0f2f886..0843b9025 100644 --- a/debian/control +++ b/debian/control @@ -33,6 +33,7 @@ Architecture: amd64 arm64 Depends: ${python3:Depends}, accel-ppp, + avahi-daemon, beep, bmon, bsdmainutils, @@ -87,7 +88,6 @@ Depends: lldpd, lm-sensors, lsscsi, - mdns-repeater, minisign, modemmanager, mtr-tiny, diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index b1092c3e5..8941f065c 100755 --- a/smoketest/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -42,7 +42,7 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check for running process - self.assertTrue(process_named_running('mdns-repeater')) + self.assertTrue(process_named_running('avahi-daemon')) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index c920920ed..55b568e48 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -28,7 +28,7 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/etc/default/mdns-repeater' +config_file = '/etc/avahi/avahi-daemon.conf' vrrp_running_file = '/run/mdns_vrrp_active' def get_config(config=None): @@ -92,12 +92,12 @@ def generate(mdns): if len(mdns['interface']) < 2: return None - render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) + render(config_file, 'mdns-repeater/avahi-daemon.tmpl', mdns) return None def apply(mdns): if not mdns or 'disable' in mdns: - call('systemctl stop mdns-repeater.service') + call('systemctl stop avahi-daemon.service') if os.path.exists(config_file): os.unlink(config_file) @@ -112,10 +112,10 @@ def apply(mdns): os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater if len(mdns['interface']) < 2: - call('systemctl stop mdns-repeater.service') + call('systemctl stop avahi-daemon.service') return None - call('systemctl restart mdns-repeater.service') + call('systemctl restart avahi-daemon.service') return None -- cgit v1.2.3 From ead10909ba9104733930bb3f59c90610138bd047 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Wed, 20 Oct 2021 17:49:15 +0000 Subject: dhcpv6-server: T3918: Fix subnets verify raise ConfigError --- src/conf_mode/dhcpv6_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 175300bb0..e6a2e4486 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -128,7 +128,7 @@ def verify(dhcpv6): # Subnets must be unique if subnet in subnets: - raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') subnets.append(subnet) # DHCPv6 requires at least one configured address range or one static mapping -- cgit v1.2.3 From 69aa8ca0aa93575c3758c9d6a86c37159848116c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 20 Oct 2021 21:20:17 +0200 Subject: tunnel: T3921: bugfix KeyError for source-address (cherry picked from commit 1312068cb9743dd4d16edd37dbed9c142724997e) --- src/conf_mode/interfaces-tunnel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 51127127d..da8624202 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -98,7 +98,7 @@ def verify(tunnel): # If tunnel source address any and key not set if tunnel['encapsulation'] in ['gre'] and \ - tunnel['source_address'] == '0.0.0.0' and \ + dict_search('source_address', tunnel) == '0.0.0.0' and \ dict_search('parameters.ip.key', tunnel) == None: raise ConfigError('Tunnel parameters ip key must be set!') @@ -111,7 +111,7 @@ def verify(tunnel): # no match on encapsulation - bail out if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: continue - new_source_address = tunnel['source_address'] + new_source_address = dict_search('source_address', tunnel) # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 orig_new_key = dict_search('parameters.ip.key', tunnel) -- cgit v1.2.3 From 1d89e5196611f06bc1d0f925fc2ac1cb4a5536ec Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 20 Oct 2021 21:34:45 +0200 Subject: mdns: T3917: move avahi configuration file to /run --- src/conf_mode/service_mdns-repeater.py | 4 ++-- src/etc/systemd/system/avahi-daemon.service.d/override.conf | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 src/etc/systemd/system/avahi-daemon.service.d/override.conf (limited to 'src/conf_mode') diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index 55b568e48..d31a0c49e 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -28,7 +28,7 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/etc/avahi/avahi-daemon.conf' +config_file = '/run/avahi-daemon/avahi-daemon.conf' vrrp_running_file = '/run/mdns_vrrp_active' def get_config(config=None): @@ -106,7 +106,7 @@ def apply(mdns): else: if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file): os.unlink(vrrp_running_file) - + if mdns['vrrp_exists'] and 'vrrp_disable' in mdns: if not os.path.exists(vrrp_running_file): os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater diff --git a/src/etc/systemd/system/avahi-daemon.service.d/override.conf b/src/etc/systemd/system/avahi-daemon.service.d/override.conf new file mode 100644 index 000000000..a9d2085f7 --- /dev/null +++ b/src/etc/systemd/system/avahi-daemon.service.d/override.conf @@ -0,0 +1,8 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/avahi-daemon/avahi-daemon.conf + +[Service] +ExecStart= +ExecStart=/usr/sbin/avahi-daemon --syslog --file /run/avahi-daemon/avahi-daemon.conf \ No newline at end of file -- cgit v1.2.3 From 28db7b15426fffc0f656e8d26db397d7bfb72aee Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Thu, 21 Oct 2021 07:59:06 -0500 Subject: use vyos read_file and write_file functions --- src/conf_mode/interfaces-openvpn.py | 45 +++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 25 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 2c8df4831..7f4aa367f 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -50,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 @@ -385,32 +386,26 @@ def verify(openvpn): # 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)): - makedir(otp_path) - open(otp_file.format(**openvpn), 'a').close() - - with tempfile.TemporaryFile(mode='w+') as fp: - with open(otp_file.format(**openvpn), 'r+') as f: - ovpn_users = f.readlines() - for client in (dict_search('server.client', openvpn) or []): - exists = None - for ovpn_user in ovpn_users: - if re.search('^' + client + ' ', ovpn_user): - fp.write(ovpn_user) - exists = 'true' - - if not exists: - random = SystemRandom() - totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) - fp.write(f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n') - - f.seek(0) - fp.seek(0) - for tmp_user in fp.readlines(): - f.write(tmp_user) - f.truncate() - - chown(otp_file.format(**openvpn), user, group) + 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 -- cgit v1.2.3 From 9c825a3457a88a4eebc6475f92332822e5102889 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 21 Oct 2021 16:58:32 +0000 Subject: dhcp: T3626: Prevent to disable only one configured network --- src/conf_mode/dhcp_server.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 28f2a4ca5..71b71879c 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -151,9 +151,15 @@ def verify(dhcp): listen_ok = False subnets = [] failover_ok = False + shared_networks = len(dhcp['shared_network_name']) + disabled_shared_networks = 0 + # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): + if 'disable' in network_config: + disabled_shared_networks += 1 + if 'subnet' not in network_config: raise ConfigError(f'No subnets defined for {network}. At least one\n' \ 'lease subnet must be configured.') @@ -243,6 +249,10 @@ def verify(dhcp): if net.overlaps(net2): raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + # Prevent 'disable' for shared-network if only one network is configured + if (shared_networks - disabled_shared_networks) < 1: + raise ConfigError(f'At least one shared network must be active!') + if 'failover' in dhcp: if not failover_ok: raise ConfigError('DHCP failover must be enabled for at least one subnet!') -- cgit v1.2.3 From 78cfb949cc6bceab744271cf23f269276b178182 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 21 Oct 2021 17:25:47 +0000 Subject: dhcp-server: T3610: Allow configuration for non-primary ip address --- src/conf_mode/dhcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 28f2a4ca5..28e40f1eb 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -226,7 +226,7 @@ def verify(dhcp): # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: - if is_subnet_connected(subnet, primary=True): + if is_subnet_connected(subnet, primary=False): listen_ok = True # Subnets must be non overlapping -- cgit v1.2.3 From 71e793d92faefcb589d3090b8af8d3e77e06b023 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 21 Oct 2021 20:31:28 +0200 Subject: ddclient: T3897: bugfix smoketest --- smoketest/scripts/cli/test_service_dns_dynamic.py | 34 +++++++++++------------ src/conf_mode/dynamic_dns.py | 4 +-- 2 files changed, 17 insertions(+), 21 deletions(-) (limited to 'src/conf_mode') diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 03fccf2c7..134254186 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -28,7 +28,10 @@ from vyos.util import read_file PROCESS_NAME = 'ddclient' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' + base_path = ['service', 'dns', 'dynamic'] +hostname = 'test.ddns.vyos.io' +interface = 'eth0' def get_config_value(key): tmp = cmd(f'sudo cat {DDCLIENT_CONF}') @@ -37,14 +40,13 @@ def get_config_value(key): return tmp class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): - def tearDown(self): # Delete DDNS configuration self.cli_delete(base_path) self.cli_commit() def test_dyndns_service(self): - ddns = ['interface', 'eth0', 'service'] + ddns = ['interface', interface, 'service'] services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit'] for service in services: @@ -52,7 +54,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): password = 'vyos_pass' zone = 'vyos.io' self.cli_delete(base_path) - self.cli_set(base_path + ddns + [service, 'host-name', 'test.ddns.vyos.io']) + self.cli_set(base_path + ddns + [service, 'host-name', hostname]) self.cli_set(base_path + ddns + [service, 'login', user]) self.cli_set(base_path + ddns + [service, 'password', password]) self.cli_set(base_path + ddns + [service, 'zone', zone]) @@ -95,7 +97,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_dyndns_rfc2136(self): # Check if DDNS service can be configured and runs - ddns = ['interface', 'eth0', 'rfc2136', 'vyos'] + ddns = ['interface', interface, 'rfc2136', 'vyos'] ddns_key_file = '/config/auth/my.key' self.cli_set(base_path + ddns + ['key', ddns_key_file]) @@ -124,19 +126,18 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) def test_dyndns_ipv6(self): - ddns = ['interface', 'eth0', 'service', 'dynv6'] - hostname = 'test.ddns.vyos.io' + ddns = ['interface', interface, 'service', 'dynv6'] proto = 'dyndns2' user = 'none' password = 'paSS_4ord' - servr = 'ddns.vyos.io' + srv = 'ddns.vyos.io' - self.cli_set(base_path + ['interface', 'eth0', 'ipv6-enable']) + self.cli_set(base_path + ['interface', interface, 'ipv6-enable']) self.cli_set(base_path + ddns + ['host-name', hostname]) self.cli_set(base_path + ddns + ['login', user]) self.cli_set(base_path + ddns + ['password', password]) self.cli_set(base_path + ddns + ['protocol', proto]) - self.cli_set(base_path + ddns + ['server', servr]) + self.cli_set(base_path + ddns + ['server', srv]) # commit changes self.cli_commit() @@ -144,21 +145,18 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - config = read_file(DDCLIENT_CONF) - protocol = get_config_value('protocol') login = get_config_value('login') pwd = get_config_value('password') server = get_config_value('server') + usev6 = get_config_value('usev6') # Check some generating config parameters - self.assertTrue(protocol == proto) - self.assertTrue(login == user) - self.assertTrue(pwd == "'" + password + "'") - self.assertTrue(server == servr) - - self.assertIn('usev6=if', config) - self.assertIn(hostname, config) + self.assertEqual(protocol, proto) + self.assertEqual(login, user) + self.assertEqual(pwd, f"'{password}'") + self.assertEqual(server, srv) + self.assertEqual(usev6, f"if, if={interface}") if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 646de6324..a31e5ed75 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -131,9 +131,7 @@ def generate(dyndns): if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, - permission=0o644) - + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) return None def apply(dyndns): -- cgit v1.2.3 From b1db3de80b8b5f4e2dcbc6d687d342986345c4b2 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 22 Oct 2021 11:55:17 +0000 Subject: hosts: T2683: Allow multiple entries for static-host-mapping --- data/templates/vyos-hostsd/hosts.tmpl | 5 +++-- interface-definitions/dns-domain-name.xml.in | 2 +- src/conf_mode/host_name.py | 2 +- src/services/vyos-hostsd | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl index 8b73c6e51..03662d562 100644 --- a/data/templates/vyos-hostsd/hosts.tmpl +++ b/data/templates/vyos-hostsd/hosts.tmpl @@ -17,8 +17,9 @@ ff02::2 ip6-allrouters {% for tag, taghosts in hosts.items() %} # {{ tag }} {% for host, hostprops in taghosts.items() if hostprops.address is defined %} -{{ "%-15s" | format(hostprops.address) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% for addr in hostprops.address %} +{{ "%-15s" | format(addr) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% endfor %} {% endfor %} {% endfor %} {% endif %} - diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index 2b1644609..005a55ab3 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -102,11 +102,11 @@ + - diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index a7135911d..87bad0dc6 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -79,7 +79,7 @@ def get_config(config=None): # system static-host-mapping for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']): hosts['static_host_mapping'][hn] = {} - hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet']) + hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet']) hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias']) return hosts diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 4c4bb036e..f4b1d0fc2 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -317,7 +317,7 @@ hosts_add_schema = op_type_schema.extend({ 'data': { str: { str: { - 'address': str, + 'address': [str], 'aliases': [str] } } -- cgit v1.2.3 From aeff049aea37115a7bbe52cd0da4d987fbccde82 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 22 Oct 2021 14:41:24 +0000 Subject: sstp: T2566: Fix verify section for pool ipv6 only (cherry picked from commit 3af310cb76d96d08151e4cdc83abcfe15484a556) --- src/conf_mode/vpn_sstp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index d1a71a5ad..68139dc47 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -58,7 +58,7 @@ def verify(sstp): verify_accel_ppp_base_service(sstp) - if not sstp['client_ip_pool']: + if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: raise ConfigError('Client IP subnet required') # -- cgit v1.2.3 From 45f815928976519ffba2ecadf197f725e7640852 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 25 Oct 2021 18:07:06 +0000 Subject: snmp: T2763: Add protocol TCP for service snmp --- data/templates/snmp/etc.snmpd.conf.tmpl | 2 +- interface-definitions/snmp.xml.in | 20 ++++++++++++++++++++ src/conf_mode/snmp.py | 9 +++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl index db2114fa1..30806ce8a 100644 --- a/data/templates/snmp/etc.snmpd.conf.tmpl +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -39,7 +39,7 @@ SysDescr {{ description }} {% endif %} # Listen -agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} +agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},{{protocol}}:161{% if ipv6_enabled %},{{protocol}}6:161{% endif %}{% endif %} # SNMP communities {% for c in communities %} diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index b0b7768d2..949536fe7 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -149,6 +149,26 @@ Oid must be 'route-table' + + + Listen protocol for SNMP + + udp tcp + + + udp + Listen protocol UDP (default) + + + tcp + Listen protocol TCP + + + ^(udp|tcp)$ + + + udp + Register a subtree for SMUX-based processing diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 23e45a5b7..2a420b193 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -52,6 +52,7 @@ default_config_data = { 'communities': [], 'smux_peers': [], 'location' : '', + 'protocol' : 'udp', 'description' : '', 'contact' : '', 'route_table': 'False', @@ -151,6 +152,9 @@ def get_config(): if conf.exists('location'): snmp['location'] = conf.return_value('location') + if conf.exists('protocol'): + snmp['protocol'] = conf.return_value('protocol') + if conf.exists('smux-peer'): snmp['smux_peers'] = conf.return_values('smux-peer') @@ -404,13 +408,14 @@ def verify(snmp): for listen in snmp['listen_address']: addr = listen[0] port = listen[1] + protocol = snmp['protocol'] if is_ipv4(addr): # example: udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port + listen = f'{protocol}:{addr}:{port}' elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port + listen = f'{protocol}6:[{addr}]:{port}' # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist -- cgit v1.2.3 From 2c82c9acbde2ccca9c7bb5e646a45fd646463afe Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 27 Oct 2021 21:54:23 +0200 Subject: vrrp: T3944: reload daemon instead of restart when already running This prevents a failover from MASTER -> BACKUP when changing any MASTER related configuration. --- src/conf_mode/vrrp.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index e8f1c1f99..c72efc61f 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -28,6 +28,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import is_systemd_service_running from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -139,7 +140,12 @@ def apply(vrrp): call(f'systemctl stop {service_name}') return None - call(f'systemctl restart {service_name}') + # XXX: T3944 - reload keepalived configuration if service is already running + # to not cause any service disruption when applying changes. + if is_systemd_service_running(service_name): + call(f'systemctl reload {service_name}') + else: + call(f'systemctl restart {service_name}') return None if __name__ == '__main__': -- cgit v1.2.3 From 0852c588d5557052af442cb1a3887f94046fa0f4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 29 Oct 2021 22:14:48 +0200 Subject: https: pki: T3642: embed CA certificate into chain if specified --- interface-definitions/https.xml.in | 1 + src/conf_mode/https.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index bb6f71744..f60df7c34 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -121,6 +121,7 @@ TLS certificates + #include #include diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index be4380462..92dc4a410 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -28,6 +28,7 @@ from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import write_file from vyos import airbag airbag.enable() @@ -139,15 +140,18 @@ def generate(https): cert_path = os.path.join(cert_dir, f'{cert_name}.pem') key_path = os.path.join(key_dir, f'{cert_name}.pem') - with open(cert_path, 'w') as f: - f.write(wrap_certificate(pki_cert['certificate'])) + server_cert = str(wrap_certificate(pki_cert['certificate'])) + if 'ca-certificate' in cert_dict: + ca_cert = cert_dict['ca-certificate'] + print(ca_cert) + server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) - with open(key_path, 'w') as f: - f.write(wrap_private_key(pki_cert['private']['key'])) + write_file(cert_path, server_cert) + write_file(key_path, wrap_private_key(pki_cert['private']['key'])) vyos_cert_data = { - "crt": cert_path, - "key": key_path + 'crt': cert_path, + 'key': key_path } for block in server_block_list: -- cgit v1.2.3 From f227987ccf41e01d4ddafb6db7b36ecf13148c78 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 13:48:15 +0100 Subject: console: T3954: bugfix RuntimeError: dictionary keys changed during iteration --- interface-definitions/system-console.xml.in | 1 + src/conf_mode/system_console.py | 70 ++++++++++++++++++----------- 2 files changed, 45 insertions(+), 26 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/system-console.xml.in b/interface-definitions/system-console.xml.in index 88f7f82a9..2897e5e97 100644 --- a/interface-definitions/system-console.xml.in +++ b/interface-definitions/system-console.xml.in @@ -74,6 +74,7 @@ ^(1200|2400|4800|9600|19200|38400|57600|115200)$ + 115200 diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 33a546bd3..19b252513 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -18,9 +18,14 @@ import os import re from vyos.config import Config -from vyos.util import call, read_file, write_file +from vyos.configdict import dict_merge +from vyos.util import call +from vyos.util import read_file +from vyos.util import write_file from vyos.template import render -from vyos import ConfigError, airbag +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag airbag.enable() by_bus_dir = '/dev/serial/by-bus' @@ -36,21 +41,27 @@ def get_config(config=None): console = conf.get_config_dict(base, get_first_key=True) # bail out early if no serial console is configured - if 'device' not in console.keys(): + if 'device' not in console: return console # convert CLI values to system values - for device in console['device'].keys(): - # no speed setting has been configured - use default value - if not 'speed' in console['device'][device].keys(): - tmp = { 'speed': '' } - if device.startswith('hvc'): - tmp['speed'] = 38400 - else: - tmp['speed'] = 115200 + default_values = defaults(base + ['device']) + for device, device_config in console['device'].items(): + if 'speed' not in device_config and device.startswith('hvc'): + # XEN console has a different default console speed + console['device'][device]['speed'] = 38400 + else: + # Merge in XML defaults - the proper way to do it + console['device'][device] = dict_merge(default_values, + console['device'][device]) + + return console - console['device'][device].update(tmp) +def verify(console): + if not console or 'device' not in console: + return None + for device in console['device']: if device.startswith('usb'): # It is much easiert to work with the native ttyUSBn name when using # getty, but that name may change across reboots - depending on the @@ -58,13 +69,13 @@ def get_config(config=None): # to its dynamic device file - and create a new dict entry for it. by_bus_device = f'{by_bus_dir}/{device}' if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): - tmp = os.path.basename(os.readlink(by_bus_device)) - # updating the dict must come as last step in the loop! - console['device'][tmp] = console['device'].pop(device) + device = os.path.basename(os.readlink(by_bus_device)) - return console + # If the device name still starts with usbXXX no matching tty was found + # and it can not be used as a serial interface + if device.startswith('usb'): + raise ConfigError(f'Device {device} does not support beeing used as tty') -def verify(console): return None def generate(console): @@ -76,20 +87,29 @@ def generate(console): call(f'systemctl stop {basename}') os.unlink(os.path.join(root, basename)) - if not console: + if not console or 'device' not in console: return None - for device in console['device'].keys(): + for device, device_config in console['device'].items(): + if device.startswith('usb'): + # It is much easiert to work with the native ttyUSBn name when using + # getty, but that name may change across reboots - depending on the + # amount of connected devices. We will resolve the fixed device name + # to its dynamic device file - and create a new dict entry for it. + by_bus_device = f'{by_bus_dir}/{device}' + if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): + device = os.path.basename(os.readlink(by_bus_device)) + config_file = base_dir + f'/serial-getty@{device}.service' getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' - render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device]) + render(config_file, 'getty/serial-getty.service.tmpl', device_config) os.symlink(config_file, getty_wants_symlink) # GRUB # For existing serial line change speed (if necessary) # Only applys to ttyS0 - if 'ttyS0' not in console['device'].keys(): + if 'ttyS0' not in console['device']: return None speed = console['device']['ttyS0']['speed'] @@ -98,7 +118,6 @@ def generate(console): return None lines = read_file(grub_config).split('\n') - p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') write = False newlines = [] @@ -122,9 +141,8 @@ def generate(console): return None def apply(console): - # reset screen blanking + # Reset screen blanking call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux /dev/tty1 2>&1') - # Reload systemd manager configuration call('systemctl daemon-reload') @@ -136,11 +154,11 @@ def apply(console): call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux /dev/tty1 2>&1') # Start getty process on configured serial interfaces - for device in console['device'].keys(): + for device in console['device']: # Only start console if it exists on the running system. If a user # detaches a USB serial console and reboots - it should not fail! if os.path.exists(f'/dev/{device}'): - call(f'systemctl start serial-getty@{device}.service') + call(f'systemctl restart serial-getty@{device}.service') return None -- cgit v1.2.3 From 17215846b512851e7df8cdfcfc06c18b1d27f763 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 14:42:04 +0100 Subject: netflow: T3953: use warning if "netflow source-ip" does not exist instead of error --- src/conf_mode/flow_accounting_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 9cae29481..0a4559ade 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -306,7 +306,7 @@ def verify(config): source_ip_presented = True break if not source_ip_presented: - raise ConfigError("Your \"netflow source-ip\" does not exist in the system") + print("Warning: your \"netflow source-ip\" does not exist in the system") # check if engine-id compatible with selected protocol version if config['netflow']['engine-id']: -- cgit v1.2.3 From 85bf315f71b411e3cdcd19793c4f7e1e5efed917 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 31 Oct 2021 14:50:47 +0100 Subject: tunnel: T3956: GRE key check must not be run on our own interface instance (cherry picked from commit e482377b29df05e60dbdb31d6276ae2030ffa2f9) --- src/conf_mode/interfaces-tunnel.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index da8624202..30f57ec0c 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -107,6 +107,10 @@ def verify(tunnel): # Check pairs tunnel source-address/encapsulation/key with exists tunnels. # Prevent the same key for 2 tunnels with same source-address/encap. T2920 for tunnel_if in Section.interfaces('tunnel'): + # It makes no sense to run the test for re-used GRE keys on our + # own interface we are currently working on + if tunnel['ifname'] == tunnel_if: + continue tunnel_cfg = get_interface_config(tunnel_if) # no match on encapsulation - bail out if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: -- cgit v1.2.3