diff options
| -rw-r--r-- | op-mode-definitions/vpn-ipsec.xml.in | 3 | ||||
| -rw-r--r-- | python/vyos/ipsec.py | 141 | ||||
| -rwxr-xr-x | src/op_mode/ipsec.py | 125 | 
3 files changed, 199 insertions, 70 deletions
| 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 @@                <tagNode name="tunnel">                  <properties>                    <help>Reset a specific tunnel for given peer</help> +                  <completionHelp> +                    <path>vpn ipsec site-to-site peer ${COMP_WORDS[3]} tunnel</path> +                  </completionHelp>                  </properties>                  <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="$6"</command>                </tagNode> 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 <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +#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 <http://www.gnu.org/licenses/>. -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): | 
