From 2b8854761c8ed419b2a2f1e02810c3f68f1d72b6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 13 Aug 2021 21:15:58 +0200 Subject: vyos.util: "harden" is_systemd_service_running() function Inspired by the comments of https://unix.stackexchange.com/a/435317 use a more robust approach. A service can be "active" but not "running" (e.g. restarting with a configuration error). We can now test if a systemd unit is "activated" and if it is "running" at all. >>> from vyos.util import is_systemd_service_active >>> from vyos.util import is_systemd_service_running >>> is_systemd_service_active('ssh') True >>> is_systemd_service_running('sshd') False >>> is_systemd_service_running('ssh') True --- python/vyos/util.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index 59f9f1c44..60171746a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -805,8 +805,16 @@ def make_incremental_progressbar(increment: float): while True: yield +def is_systemd_service_active(service): + """ Test is a specified systemd service is activated. + Returns True if service is active, false otherwise. + Copied from: https://unix.stackexchange.com/a/435317 """ + tmp = cmd(f'systemctl show --value -p ActiveState {service}') + return bool((tmp == 'active')) + def is_systemd_service_running(service): """ Test is a specified systemd service is actually running. - Returns True if service is running, false otherwise. """ - tmp = run(f'systemctl is-active --quiet {service}') - return bool((tmp == 0)) + Returns True if service is running, false otherwise. + Copied from: https://unix.stackexchange.com/a/435317 """ + tmp = cmd(f'systemctl show --value -p SubState {service}') + return bool((tmp == 'running')) -- cgit v1.2.3 From a74e67a778a6c698e44cbc6c5d184d03c9c12396 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 14 Aug 2021 20:13:31 +0200 Subject: vyos.util: T1503: use build in methods to determine current user for commit_in_progress() --- python/vyos/util.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index 60171746a..8af46a6ee 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -562,12 +562,13 @@ def commit_in_progress(): # Since this will be used in scripts that modify the config outside of the CLI # framework, those knowingly have root permissions. # For everything else, we add a safeguard. - from psutil import process_iter, NoSuchProcess + from psutil import process_iter + from psutil import NoSuchProcess + from getpass import getuser from vyos.defaults import commit_lock - idu = cmd('/usr/bin/id -u') - if idu != '0': - raise OSError("This functions needs root permissions to return correct results") + if getuser() != 'root': + raise OSError('This functions needs to be run as root to return correct results!') for proc in process_iter(): try: -- cgit v1.2.3 From 4523e9c897b3fa8d12c1b16c830c01820fee5583 Mon Sep 17 00:00:00 2001 From: zsdc Date: Thu, 26 Aug 2021 18:15:36 +0300 Subject: wireguard: T3763: Added check for listening port availability Each wireguard interface requires a unique port for in and out connections. This commit adds the new `vyos.util` function - `check_port_availability`, and uses it to be sure that a port that is planned to be used for wireguard interface is truly available and not used by any other services (not only other wireguard interfaces). --- python/vyos/util.py | 39 +++++++++++++++++++++++++++++++++++ src/conf_mode/interfaces-wireguard.py | 5 +++++ 2 files changed, 44 insertions(+) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index 8af46a6ee..fc2834a97 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -819,3 +819,42 @@ def is_systemd_service_running(service): Copied from: https://unix.stackexchange.com/a/435317 """ tmp = cmd(f'systemctl show --value -p SubState {service}') return bool((tmp == 'running')) + +def check_port_availability(ipaddress, port, protocol): + """ + Check if port is available and not used by any service + Return False if a port is busy or IP address does not exists + Should be used carefully for services that can start listening + dynamically, because IP address may be dynamic too + """ + from socketserver import TCPServer, UDPServer + from ipaddress import ip_address + + # verify arguments + try: + ipaddress = ip_address(ipaddress).compressed + except: + print(f'The {ipaddress} is not a valid IPv4 or IPv6 address') + return + if port not in range(1, 65536): + print(f'The port number {port} is not in the 1-65535 range') + return + if protocol not in ['tcp', 'udp']: + print( + f'The protocol {protocol} is not supported. Only tcp and udp are allowed' + ) + return + + # check port availability + try: + if protocol == 'tcp': + server = TCPServer((ipaddress, port), None, bind_and_activate=True) + if protocol == 'udp': + server = UDPServer((ipaddress, port), None, bind_and_activate=True) + server.server_close() + return True + except: + print( + f'The {protocol} port {port} on the {ipaddress} is busy or unavailable' + ) + return False diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 4c566a5ad..ad3ddcba2 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -30,6 +30,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import WireGuardIf from vyos.util import check_kmod +from vyos.util import check_port_availability from vyos import ConfigError from vyos import airbag airbag.enable() @@ -73,6 +74,10 @@ def verify(wireguard): if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') + if 'port' in wireguard and check_port_availability( + '0.0.0.0', int(wireguard['port']), 'udp') is not True: + raise ConfigError('The port cannot be used for the interface') + # run checks on individual configured WireGuard peer for tmp in wireguard['peer']: peer = wireguard['peer'][tmp] -- cgit v1.2.3 From eb11d4b688d883d0c1d150b00eee40b54df42b32 Mon Sep 17 00:00:00 2001 From: zsdc Date: Thu, 26 Aug 2021 23:18:29 +0300 Subject: vyos.util: T3763: Optimized the check_port_availability function `print` was removed or replaced to `ValueError`, where possible. --- python/vyos/util.py | 12 +++--------- src/conf_mode/interfaces-wireguard.py | 9 ++++++--- 2 files changed, 9 insertions(+), 12 deletions(-) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index fc2834a97..93a2f6640 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -834,16 +834,13 @@ def check_port_availability(ipaddress, port, protocol): try: ipaddress = ip_address(ipaddress).compressed except: - print(f'The {ipaddress} is not a valid IPv4 or IPv6 address') - return + raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address') if port not in range(1, 65536): - print(f'The port number {port} is not in the 1-65535 range') - return + raise ValueError(f'The port number {port} is not in the 1-65535 range') if protocol not in ['tcp', 'udp']: - print( + raise ValueError( f'The protocol {protocol} is not supported. Only tcp and udp are allowed' ) - return # check port availability try: @@ -854,7 +851,4 @@ def check_port_availability(ipaddress, port, protocol): server.server_close() return True except: - print( - f'The {protocol} port {port} on the {ipaddress} is busy or unavailable' - ) return False diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index ad3ddcba2..68181465e 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -74,9 +74,12 @@ def verify(wireguard): if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') - if 'port' in wireguard and check_port_availability( - '0.0.0.0', int(wireguard['port']), 'udp') is not True: - raise ConfigError('The port cannot be used for the interface') + listen_port = int(wireguard['port']) + if 'port' in wireguard and check_port_availability('0.0.0.0', listen_port, + 'udp') is not True: + raise ConfigError( + f'The UDP port {listen_port} is busy or unavailable and cannot be used for the interface' + ) # run checks on individual configured WireGuard peer for tmp in wireguard['peer']: -- cgit v1.2.3 From 9d0c37fbbc91acc9f2c0f2abaab360479e451f0f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 6 Sep 2021 12:02:56 +0200 Subject: vyos.util: T2755: rename dict_search() function args to match other implementations --- python/vyos/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index 93a2f6640..18b7f5fcb 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -692,21 +692,21 @@ def find_device_file(device): return None -def dict_search(path, my_dict): - """ Traverse Python dictionary (my_dict) delimited by dot (.). +def dict_search(path, dict_object): + """ Traverse Python dictionary (dict_object) delimited by dot (.). Return value of key if found, None otherwise. - This is faster implementation then jmespath.search('foo.bar', my_dict)""" - if not isinstance(my_dict, dict) or not path: + This is faster implementation then jmespath.search('foo.bar', dict_object)""" + if not isinstance(dict_object, dict) or not path: return None parts = path.split('.') inside = parts[:-1] if not inside: - if path not in my_dict: + if path not in dict_object: return None - return my_dict[path] - c = my_dict + return dict_object[path] + c = dict_object for p in parts[:-1]: c = c.get(p, {}) return c.get(parts[-1], None) -- cgit v1.2.3 From acc6e461a92b14091fef9f49514f26364579391d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 6 Sep 2021 12:09:57 +0200 Subject: vyos.util: add function to search a key recursively in a dictionary data = { 'interfaces': {'dummy': {'dum0': {'address': ['192.0.2.17/29']}}, 'ethernet': {'eth0': {'address': ['2001:db8::1/64', '192.0.2.1/29'], 'description': 'Test123', 'duplex': 'auto', 'hw_id': '00:00:00:00:00:01', 'speed': 'auto'}, 'eth1': {'address': ['192.0.2.9/29'], 'description': 'Test456', 'duplex': 'auto', 'hw_id': '00:00:00:00:00:02', 'speed': 'auto'}}} } dict_search_recursive(data, 'hw_id') will yield both '00:00:00:00:00:01' and '00:00:00:00:00:02' as generator object. --- python/vyos/util.py | 17 +++++++++++++++++ src/tests/test_dict_search.py | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) (limited to 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index 18b7f5fcb..b41c5b346 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -724,6 +724,23 @@ def dict_search_args(dict_object, *path): dict_object = dict_object[item] return dict_object +def dict_search_recursive(dict_object, key): + """ Traverse a dictionary recurisvely and return the value of the key + we are looking for. + + Thankfully copied from https://stackoverflow.com/a/19871956 + """ + if isinstance(dict_object, list): + for i in dict_object: + for x in dict_search_recursive(i, key): + yield x + elif isinstance(dict_object, dict): + if key in dict_object: + yield dict_object[key] + for j in dict_object.values(): + for x in dict_search_recursive(j, key): + yield x + def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. diff --git a/src/tests/test_dict_search.py b/src/tests/test_dict_search.py index 991722f0f..1028437b2 100644 --- a/src/tests/test_dict_search.py +++ b/src/tests/test_dict_search.py @@ -16,13 +16,25 @@ from unittest import TestCase from vyos.util import dict_search +from vyos.util import dict_search_recursive data = { 'string': 'fooo', 'nested': {'string': 'bar', 'empty': '', 'list': ['foo', 'bar']}, 'non': {}, 'list': ['bar', 'baz'], - 'dict': {'key_1': {}, 'key_2': 'vyos'} + 'dict': {'key_1': {}, 'key_2': 'vyos'}, + 'interfaces': {'dummy': {'dum0': {'address': ['192.0.2.17/29']}}, + 'ethernet': {'eth0': {'address': ['2001:db8::1/64', '192.0.2.1/29'], + 'description': 'Test123', + 'duplex': 'auto', + 'hw_id': '00:00:00:00:00:01', + 'speed': 'auto'}, + 'eth1': {'address': ['192.0.2.9/29'], + 'description': 'Test456', + 'duplex': 'auto', + 'hw_id': '00:00:00:00:00:02', + 'speed': 'auto'}}} } class TestDictSearch(TestCase): @@ -63,3 +75,10 @@ class TestDictSearch(TestCase): # TestDictSearch: Return list items when querying nested list self.assertEqual(dict_search('nested.list', None), None) self.assertEqual(dict_search(None, data), None) + + def test_dict_search_recursive(self): + # Test nested search in dictionary + tmp = list(dict_search_recursive(data, 'hw_id')) + self.assertEqual(len(tmp), 2) + tmp = list(dict_search_recursive(data, 'address')) + self.assertEqual(len(tmp), 3) -- cgit v1.2.3 From 63fbd8c663c8c42ad178d6f0694f20bb98acf01a 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 'python/vyos/util.py') diff --git a/python/vyos/util.py b/python/vyos/util.py index b41c5b346..849b27d3b 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 6be4e918b..274bc655e 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -45,9 +45,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 @@ -449,7 +449,6 @@ def verify(openvpn): def generate_pki_files(openvpn): pki = openvpn['pki'] - if not pki: return None @@ -457,16 +456,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: @@ -475,20 +469,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: @@ -497,19 +486,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: @@ -518,11 +502,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'] @@ -530,11 +511,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'] @@ -570,7 +548,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: @@ -598,10 +576,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