diff options
Diffstat (limited to 'src/etc/opennhrp/opennhrp-script.py')
| -rwxr-xr-x | src/etc/opennhrp/opennhrp-script.py | 315 | 
1 files changed, 253 insertions, 62 deletions
| diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index 8274e6564..a5293c97e 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -18,44 +18,120 @@ import os  import re  import sys  import vici +  from json import loads +from pathlib import Path +from vyos.logger import getLogger  from vyos.util import cmd  from vyos.util import process_named_running -NHRP_CONFIG = "/run/opennhrp/opennhrp.conf" +NHRP_CONFIG: str = '/run/opennhrp/opennhrp.conf' + + +def vici_get_ipsec_uniqueid(conn: str, src_nbma: str, +                            dst_nbma: str) -> list[str]: +    """ Find and return IKE SAs by src nbma and dst nbma + +    Args: +        conn (str): a connection name +        src_nbma (str): an IP address of NBMA source +        dst_nbma (str): an IP address of NBMA destination + +    Returns: +        list: a list of IKE connections that match a criteria +    """ +    if not conn or not src_nbma or not dst_nbma: +        logger.error( +            f'Incomplete input data for resolving IKE unique ids: ' +            f'conn: {conn}, src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') +        return [] + +    try: +        logger.info( +            f'Resolving IKE unique ids for: conn: {conn}, ' +            f'src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') +        session: vici.Session = vici.Session() +        list_ikeid: list[str] = [] +        list_sa = session.list_sas({'ike': conn}) +        for sa in list_sa: +            if sa[conn]['local-host'].decode('ascii') == src_nbma \ +                    and sa[conn]['remote-host'].decode('ascii') == dst_nbma: +                list_ikeid.append(sa[conn]['uniqueid'].decode('ascii')) +        return list_ikeid +    except Exception as err: +        logger.error(f'Unable to find unique ids for IKE: {err}') +        return [] + + +def vici_ike_terminate(list_ikeid: list[str]) -> bool: +    """Terminating IKE SAs by list of IKE IDs + +    Args: +        list_ikeid (list[str]): a list of IKE ids to terminate + +    Returns: +        bool: result of termination action +    """ +    if not list: +        logger.warning('An empty list for termination was provided') +        return False + +    try: +        session = vici.Session() +        for ikeid in list_ikeid: +            logger.info(f'Terminating IKE SA with id {ikeid}') +            session.terminate({'ike-id': ikeid, 'timeout': '-1'}) +        return True +    except Exception as err: +        logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') +        return False + +def parse_type_ipsec(interface: str) -> tuple[str, str]: +    """Get DMVPN Type and NHRP Profile from the configuration -def parse_type_ipsec(interface): -    with open(NHRP_CONFIG, 'r') as f: -        lines = f.readlines() -        match = rf'^interface {interface} #(hub|spoke)(?:\s([\w-]+))?$' -        for line in lines: -            m = re.match(match, line) -            if m: -                return m[1], m[2] -    return None, None +    Args: +        interface (str): a name of interface + +    Returns: +        tuple[str, str]: `peer_type` and `profile_name` +    """ +    if not interface: +        logger.error('Cannot find peer type - no input provided') +        return '', '' + +    config_file: str = Path(NHRP_CONFIG).read_text() +    regex: str = rf'^interface {interface} #(?P<peer_type>hub|spoke) ?(?P<profile_name>[^\n]*)$' +    match = re.search(regex, config_file, re.M) +    if match: +        return match.groupdict()['peer_type'], match.groupdict()[ +            'profile_name'] +    return '', ''  def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None:      """Add a route to a NBMA peer      Args: -        nmba_src (str): a local IP address +        nbma_src (str): a local IP address          nbma_dst (str): a remote IP address          mtu (str): a MTU for a route      """ +    logger.info(f'Adding route from {nbma_src} to {nbma_dst} with MTU {mtu}')      # Find routes to a peer -    route_get_cmd = f'sudo ip -j route get {nbma_dst} from {nbma_src}' +    route_get_cmd: str = f'sudo ip --json route get {nbma_dst} from {nbma_src}'      try:          route_info_data = loads(cmd(route_get_cmd))      except Exception as err: -        print(f'Unable to find a route to {nbma_dst}: {err}') +        logger.error(f'Unable to find a route to {nbma_dst}: {err}') +        return      # Check if an output has an expected format      if not isinstance(route_info_data, list): -        print(f'Garbage returned from the "{route_get_cmd}" command: \ -            {route_info_data}') +        logger.error( +            f'Garbage returned from the "{route_get_cmd}" ' +            f'command: {route_info_data}')          return      # Add static routes to a peer @@ -76,104 +152,217 @@ def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None:          try:              cmd(route_add_cmd)          except Exception as err: -            print(f'Unable to add a route using command "{route_add_cmd}": \ -                    {err}') +            logger.error( +                f'Unable to add a route using command "{route_add_cmd}": ' +                f'{err}') -def vici_initiate(conn, child_sa, src_addr, dest_addr): -    try: -        session = vici.Session() -        logs = session.initiate({ -            'ike': conn, -            'child': child_sa, -            'timeout': '-1', -            'my-host': src_addr, -            'other-host': dest_addr -        }) -        for log in logs: -            message = log['msg'].decode('ascii') -            print('INIT LOG:', message) -        return True -    except: -        return None +def vici_initiate(conn: str, child_sa: str, src_addr: str, +                  dest_addr: str) -> bool: +    """Initiate IKE SA connection with specific peer +    Args: +        conn (str): an IKE connection name +        child_sa (str): a child SA profile name +        src_addr (str): NBMA local address +        dest_addr (str): NBMA address of a peer -def vici_terminate(conn, child_sa, src_addr, dest_addr): +    Returns: +        bool: a result of initiation command +    """ +    logger.info( +        f'Trying to initiate connection. Name: {conn}, child sa: {child_sa}, ' +        f'src_addr: {src_addr}, dst_addr: {dest_addr}')      try:          session = vici.Session() -        logs = session.terminate({ +        session.initiate({              'ike': conn,              'child': child_sa,              'timeout': '-1',              'my-host': src_addr,              'other-host': dest_addr          }) -        for log in logs: -            message = log['msg'].decode('ascii') -            print('TERM LOG:', message)          return True -    except: -        return None +    except Exception as err: +        logger.error(f'Unable to initiate connection {err}') +        return False + + +def vici_terminate(conn: str, src_addr: str, dest_addr: str) -> None: +    """Find and terminate IKE SAs by local NBMA and remote NBMA addresses + +    Args: +        conn (str): IKE connection name +        src_addr (str): NBMA local address +        dest_addr (str): NBMA address of a peer +    """ +    logger.info( +        f'Terminating IKE connection {conn} between {src_addr} ' +        f'and {dest_addr}') +    ikeid_list: list[str] = vici_get_ipsec_uniqueid(conn, src_addr, dest_addr) -def iface_up(interface): -    cmd(f'sudo ip route flush proto 42 dev {interface}') -    cmd(f'sudo ip neigh flush dev {interface}') +    if not ikeid_list: +        logger.warning( +            f'No active sessions found for IKE profile {conn}, ' +            f'local NBMA {src_addr}, remote NBMA {dest_addr}') +    else: +        vici_ike_terminate(ikeid_list) -def peer_up(dmvpn_type, conn): -    # src_addr = os.getenv('NHRP_SRCADDR') +def iface_up(interface: str) -> None: +    """Proceed tunnel interface UP event + +    Args: +        interface (str): an interface name +    """ +    if not interface: +        logger.warning('No interface name provided for UP event') + +    logger.info(f'Turning up interface {interface}') +    try: +        cmd(f'sudo ip route flush proto 42 dev {interface}') +        cmd(f'sudo ip neigh flush dev {interface}') +    except Exception as err: +        logger.error( +            f'Unable to flush route on interface "{interface}": {err}') + + +def peer_up(dmvpn_type: str, conn: str) -> None: +    """Proceed NHRP peer UP event + +    Args: +        dmvpn_type (str): a type of peer +        conn (str): an IKE profile name +    """ +    logger.info(f'Peer UP event for {dmvpn_type} using IKE profile {conn}')      src_nbma = os.getenv('NHRP_SRCNBMA') -    # dest_addr = os.getenv('NHRP_DESTADDR')      dest_nbma = os.getenv('NHRP_DESTNBMA')      dest_mtu = os.getenv('NHRP_DESTMTU') +    if not src_nbma or not dest_nbma: +        logger.error( +            f'Can not get NHRP NBMA addresses: local {src_nbma}, ' +            f'remote {dest_nbma}') +        return + +    logger.info(f'NBMA addresses: local {src_nbma}, remote {dest_nbma}')      if dest_mtu:          add_peer_route(src_nbma, dest_nbma, dest_mtu) -      if conn and dmvpn_type == 'spoke' and process_named_running('charon'): -        vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) +        vici_terminate(conn, src_nbma, dest_nbma)          vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma) -def peer_down(dmvpn_type, conn): +def peer_down(dmvpn_type: str, conn: str) -> None: +    """Proceed NHRP peer DOWN event + +    Args: +        dmvpn_type (str): a type of peer +        conn (str): an IKE profile name +    """ +    logger.info(f'Peer DOWN event for {dmvpn_type} using IKE profile {conn}') +      src_nbma = os.getenv('NHRP_SRCNBMA')      dest_nbma = os.getenv('NHRP_DESTNBMA') +    if not src_nbma or not dest_nbma: +        logger.error( +            f'Can not get NHRP NBMA addresses: local {src_nbma}, ' +            f'remote {dest_nbma}') +        return + +    logger.info(f'NBMA addresses: local {src_nbma}, remote {dest_nbma}')      if conn and dmvpn_type == 'spoke' and process_named_running('charon'): -        vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) +        vici_terminate(conn, src_nbma, dest_nbma) +    try: +        cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') +    except Exception as err: +        logger.error( +            f'Unable to del route from {src_nbma} to {dest_nbma}: {err}') -    cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') +def route_up(interface: str) -> None: +    """Proceed NHRP route UP event + +    Args: +        interface (str): an interface name +    """ +    logger.info(f'Route UP event for interface {interface}') -def route_up(interface):      dest_addr = os.getenv('NHRP_DESTADDR')      dest_prefix = os.getenv('NHRP_DESTPREFIX')      next_hop = os.getenv('NHRP_NEXTHOP') -    cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \ -        via {next_hop} dev {interface}') -    cmd('sudo ip route flush cache') +    if not dest_addr or not dest_prefix or not next_hop: +        logger.error( +            f'Can not get route details: dest_addr {dest_addr}, ' +            f'dest_prefix {dest_prefix}, next_hop {next_hop}') +        return + +    logger.info( +        f'Route details: dest_addr {dest_addr}, dest_prefix {dest_prefix}, ' +        f'next_hop {next_hop}') +    try: +        cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \ +                via {next_hop} dev {interface}') +        cmd('sudo ip route flush cache') +    except Exception as err: +        logger.error( +            f'Unable replace or flush route to {dest_addr}/{dest_prefix} ' +            f'via {next_hop} dev {interface}: {err}') + + +def route_down(interface: str) -> None: +    """Proceed NHRP route DOWN event + +    Args: +        interface (str): an interface name +    """ +    logger.info(f'Route DOWN event for interface {interface}') -def route_down(interface):      dest_addr = os.getenv('NHRP_DESTADDR')      dest_prefix = os.getenv('NHRP_DESTPREFIX') -    cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') -    cmd('sudo ip route flush cache') +    if not dest_addr or not dest_prefix: +        logger.error( +            f'Can not get route details: dest_addr {dest_addr}, ' +            f'dest_prefix {dest_prefix}') +        return + +    logger.info( +        f'Route details: dest_addr {dest_addr}, dest_prefix {dest_prefix}') +    try: +        cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') +        cmd('sudo ip route flush cache') +    except Exception as err: +        logger.error( +            f'Unable delete or flush route to {dest_addr}/{dest_prefix}: ' +            f'{err}')  if __name__ == '__main__': +    logger = getLogger('opennhrp-script', syslog=True) +    logger.debug( +        f'Running script with arguments: {sys.argv}, ' +        f'environment: {os.environ}') +      action = sys.argv[1]      interface = os.getenv('NHRP_INTERFACE') -    dmvpn_type, profile_name = parse_type_ipsec(interface) -    dmvpn_conn = None +    if not interface: +        logger.error('Can not get NHRP interface name') +        sys.exit(1) -    if profile_name: -        dmvpn_conn = f'dmvpn-{profile_name}-{interface}' +    dmvpn_type, profile_name = parse_type_ipsec(interface) +    if not dmvpn_type: +        logger.info(f'Interface {interface} is not NHRP tunnel') +        sys.exit() +    dmvpn_conn: str = '' +    if profile_name: +        dmvpn_conn: str = f'dmvpn-{profile_name}-{interface}'      if action == 'interface-up':          iface_up(interface)      elif action == 'peer-register': @@ -186,3 +375,5 @@ if __name__ == '__main__':          route_up(interface)      elif action == 'route-down':          route_down(interface) + +    sys.exit() | 
