From f4ecfaf1cd1ad7b7ea9692df5162c998add1e0c1 Mon Sep 17 00:00:00 2001 From: aapostoliuk Date: Fri, 24 Feb 2023 13:43:36 +0200 Subject: ipsec: T4985: Changed 'reset vpn ipsec-peer' to use vici library 1. Changed reset IPSEC, IKE SAs to use vici library. 2. Created package vyos.ipsec to communicate with vici library. --- op-mode-definitions/vpn-ipsec.xml.in | 3 + python/vyos/ipsec.py | 141 +++++++++++++++++++++++++++++++++++ src/op_mode/ipsec.py | 125 ++++++++++++++----------------- 3 files changed, 199 insertions(+), 70 deletions(-) create mode 100644 python/vyos/ipsec.py diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index eca9f6fd9..ee006a2d5 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -18,6 +18,9 @@ Reset a specific tunnel for given peer + + vpn ipsec site-to-site peer ${COMP_WORDS[3]} tunnel + sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="$6" diff --git a/python/vyos/ipsec.py b/python/vyos/ipsec.py new file mode 100644 index 000000000..cb7c39ff6 --- /dev/null +++ b/python/vyos/ipsec.py @@ -0,0 +1,141 @@ +# Copyright 2020-2023 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 . + +#Package to communicate with Strongswan VICI + +class ViciInitiateError(Exception): + """ + VICI can't initiate a session. + """ + pass +class ViciCommandError(Exception): + """ + VICI can't execute a command by any reason. + """ + pass + +def get_vici_sas(): + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError("IPsec not initialized") + sas = list(session.list_sas()) + return sas + + +def get_vici_connections(): + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError("IPsec not initialized") + connections = list(session.list_conns()) + return connections + + +def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list: + """ + Find sas by IKE_SA name and/or CHILD_SA name + and return list of OrdinaryDicts with SASs info + If tunnel is not None return value is list of OrdenaryDicts contained only + CHILD_SAs wich names equal tunnel value. + :param ike_name: IKE SA name + :type ike_name: str + :param tunnel: CHILD SA name + :type tunnel: str + :return: list of Ordinary Dicts with SASs + :rtype: list + """ + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError("IPsec not initialized") + vici_dict = {} + if ike_name: + vici_dict['ike'] = ike_name + if tunnel: + vici_dict['child'] = tunnel + try: + sas = list(session.list_sas(vici_dict)) + return sas + except Exception: + raise ViciCommandError(f'Failed to get SAs') + + +def terminate_vici_ikeid_list(ike_id_list: list) -> None: + """ + Terminate IKE SAs by their id that contained in the list + :param ike_id_list: list of IKE SA id + :type ike_id_list: list + """ + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError("IPsec not initialized") + try: + for ikeid in ike_id_list: + session_generator = session.terminate( + {'ike-id': ikeid, 'timeout': '-1'}) + # a dummy `for` loop is required because of requirements + # from vici. Without a full iteration on the output, the + # command to vici may not be executed completely + for _ in session_generator: + pass + except Exception: + raise ViciCommandError( + f'Failed to terminate SA for IKE ids {ike_id_list}') + + +def terminate_vici_by_name(ike_name: str, child_name: str) -> None: + """ + Terminate IKE SAs by name if CHILD SA name is None. + Terminate CHILD SAs by name if CHILD SA name is specified + :param ike_name: IKE SA name + :type ike_name: str + :param child_name: CHILD SA name + :type child_name: str + """ + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError("IPsec not initialized") + try: + vici_dict: dict= {} + if ike_name: + vici_dict['ike'] = ike_name + if child_name: + vici_dict['child'] = child_name + session_generator = session.terminate(vici_dict) + # a dummy `for` loop is required because of requirements + # from vici. Without a full iteration on the output, the + # command to vici may not be executed completely + for _ in session_generator: + pass + except Exception: + if child_name: + raise ViciCommandError( + f'Failed to terminate SA for IPSEC {child_name}') + else: + raise ViciCommandError( + f'Failed to terminate SA for IKE {ike_name}') diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 63fa05885..8e76f4cc0 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -14,25 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import re import sys import typing -from collections import OrderedDict from hurry import filesize from re import split as re_split from tabulate import tabulate -from subprocess import TimeoutExpired -from vyos.util import call from vyos.util import convert_data from vyos.util import seconds_to_human import vyos.opmode - - -SWANCTL_CONF = '/etc/swanctl/swanctl.conf' +import vyos.ipsec def _convert(text): @@ -43,22 +37,13 @@ def _alphanum_key(key): return [_convert(c) for c in re_split('([0-9]+)', str(key))] -def _get_vici_sas(): - from vici import Session as vici_session - - try: - session = vici_session() - except Exception: - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - sas = list(session.list_sas()) - return sas - - def _get_raw_data_sas(): - get_sas = _get_vici_sas() - sas = convert_data(get_sas) - return sas - + try: + get_sas = vyos.ipsec.get_vici_sas() + sas = convert_data(get_sas) + return sas + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) def _get_formatted_output_sas(sas): sa_data = [] @@ -139,22 +124,14 @@ def _get_formatted_output_sas(sas): # Connections block -def _get_vici_connections(): - from vici import Session as vici_session - - try: - session = vici_session() - except Exception: - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - connections = list(session.list_conns()) - return connections - def _get_convert_data_connections(): - get_connections = _get_vici_connections() - connections = convert_data(get_connections) - return connections - + try: + get_connections = vyos.ipsec.get_vici_connections() + connections = convert_data(get_connections) + return connections + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: """Get parent SA proposals by connection name @@ -239,7 +216,8 @@ def _get_child_sa_state(connection_name: str, tunnel_name: str, # Get all child SA states # there can be multiple SAs per tunnel child_sa_states = [ - v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name + v['state'] for k, v in child_sas.items() if + v['name'] == tunnel_name ] return 'up' if 'INSTALLED' in child_sa_states else child_sa @@ -406,39 +384,46 @@ def _get_formatted_output_conections(data): # Connections block end -def get_peer_connections(peer, tunnel): - search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*' - matches = [] - if not os.path.exists(SWANCTL_CONF): - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - suffix = None if tunnel is None else (f'tunnel-{tunnel}' if - tunnel.isnumeric() else tunnel) - with open(SWANCTL_CONF, 'r') as f: - for line in f.readlines(): - result = re.match(search, line) - if result: - if tunnel is None: - matches.append(result[1]) - else: - if result[2] == suffix: - matches.append(result[1]) - return matches - - -def reset_peer(peer: str, tunnel:typing.Optional[str] = None): - conns = get_peer_connections(peer, tunnel) - - if not conns: - raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting') - - for conn in conns: - try: - call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) - call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) - except TimeoutExpired as e: - raise vyos.opmode.InternalError(f'Timed out while resetting {conn}') - - print('Peer reset result: success') +def _get_childsa_id_list(ike_sas: list) -> list: + """ + Generate list of CHILD SA ids based on list of OrderingDict + wich is returned by vici + :param ike_sas: list of IKE SAs generated by vici + :type ike_sas: list + :return: list of IKE SAs ids + :rtype: list + """ + list_childsa_id: list = [] + for ike in ike_sas: + for ike_sa in ike.values(): + for child_sa in ike_sa['child-sas'].values(): + list_childsa_id.append(child_sa['uniqueid'].decode('ascii')) + return list_childsa_id + + +def reset_peer(peer: str, tunnel: typing.Optional[str] = None): + # Convert tunnel to Strongwan format of CHILD_SA + if tunnel: + if tunnel.isnumeric(): + tunnel = f'{peer}-tunnel-{tunnel}' + elif tunnel == 'vti': + tunnel = f'{peer}-vti' + try: + sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel) + + if not sa_list: + raise vyos.opmode.IncorrectValue('Peer not found, aborting') + if tunnel and sa_list: + childsa_id_list: list = _get_childsa_id_list(sa_list) + if not childsa_id_list: + raise vyos.opmode.IncorrectValue( + 'Peer or tunnel(s) not found, aborting') + vyos.ipsec.terminate_vici_by_name(peer, tunnel) + print('Peer reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.IncorrectValue(err) def show_sa(raw: bool): -- cgit v1.2.3